Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions candy/plugin-deploy-pod/config_remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"strings"

"github.com/opencharly/sdk"
"github.com/opencharly/sdk/kit"
pb "github.com/opencharly/spec/proto"
"github.com/opencharly/spec/spec"
Expand All @@ -17,31 +18,43 @@ import (
// VERBATIM (no seams needed — purely resolveBoxName + IsDirectDeploy + podman/systemctl exec +
// findPodSidecarQuadlets, all portable). Distinct from `charly remove`/OpPostTeardown, which tears
// down the whole deploy; this disables the quadlet service (the quadlet file remains).
func invokeConfigRemove(_ context.Context, req *pb.InvokeRequest) (*pb.InvokeReply, error) {
func invokeConfigRemove(ctx context.Context, req *pb.InvokeRequest) (*pb.InvokeReply, error) {
// The executor is OPTIONAL here: remove is driven from the runtime configuration and
// needs no venue work, so an Invoke with no executor attached must keep working — it
// simply resolves the selection from the runtime level (loadDeploy degrades to nil).
ex, _ := sdk.ExecutorFromInvoke(req.GetExecutorBrokerId())
var r spec.PodConfigRemoveRequest
if err := json.Unmarshal(req.GetParamsJson(), &r); err != nil {
return nil, fmt.Errorf("plugin-deploy-pod config-remove: decode request: %w", err)
}
if err := runPodConfigRemove(&r); err != nil {
if err := runPodConfigRemove(ctx, ex, &r); err != nil {
return nil, err
}
return marshalReply(spec.PodConfigRemoveReply{})
}

func runPodConfigRemove(c *spec.PodConfigRemoveRequest) error {
func runPodConfigRemove(ctx context.Context, ex *sdk.Executor, c *spec.PodConfigRemoveRequest) error {
rt, err := kit.ResolveRuntime()
if err != nil {
return err
}
boxName := resolveBoxName(c.Box)
dc, err := loadDeploy(ctx, ex, "charly config remove")
if err != nil {
return err
}
sel, err := resolvePodSelectionFromConfig(dc, boxName, c.Instance, rt)
if err != nil {
return err
}

if rt.RunMode == "direct" || IsDirectDeploy(boxName, c.Instance) {
name := kit.ContainerNameInstance(boxName, c.Instance)
stop := engineStopArgv(rt.RunEngine, name)
stop := engineStopArgv(sel.Target, name)
_ = exec.Command(stop[0], stop[1:]...).Run()
rm := engineRemoveArgv(rt.RunEngine, name)
rm := engineRemoveArgv(sel.Target, name)
if out, err := exec.Command(rm[0], rm[1:]...).CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: %s rm -f %s: %v\n%s", rt.RunEngine, name, err, strings.TrimSpace(string(out)))
fmt.Fprintf(os.Stderr, "Warning: %s rm -f %s: %v\n%s", sel.Engine, name, err, strings.TrimSpace(string(out)))
}
if err := removeDirectDeployMarker(boxName, c.Instance); err != nil {
fmt.Fprintf(os.Stderr, "Warning: removing direct-mode marker: %v\n", err)
Expand Down
4 changes: 3 additions & 1 deletion candy/plugin-deploy-pod/config_remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ func TestRunPodConfigRemove_DirectModeAllowed(t *testing.T) {
_ = os.Setenv("CHARLY_RUN_MODE", "direct")
defer os.Unsetenv("CHARLY_RUN_MODE") //nolint:errcheck

err := runPodConfigRemove(&spec.PodConfigRemoveRequest{Box: "fedora-test"})
// A nil executor is the proof that no overlay read is required here: loadDeploy degrades
// to (nil, nil) with a warning, and the selection falls back to the runtime level.
err := runPodConfigRemove(t.Context(), nil, &spec.PodConfigRemoveRequest{Box: "fedora-test"})
// Direct-mode remove of a non-existent deploy is best-effort — podman rm prints a
// warning but returns nil. The pre-cutover "run_mode=quadlet required" hard error must NOT fire.
if err != nil && strings.Contains(err.Error(), "run_mode=quadlet") {
Expand Down
45 changes: 27 additions & 18 deletions candy/plugin-deploy-pod/config_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@ func runPodConfigSetup(ctx context.Context, ex *sdk.Executor, c *spec.PodConfigS
default:
return fmt.Errorf("charly config requires run_mode=quadlet, systemd-unit, or direct (current: %s)", rt.RunMode)
}
// A rootful target is explicit, and this plugin cannot finish one safely: the image store
// staging runs rootless and system-scope unit emission is unimplemented. Refuse with the
// remedy rather than silently deploying the rootless arm the operator did not ask for.
if rootfulUnsupported(rt) {
return rootfulRefusal(rt.RunEngine, rt.RunMode)
}
if c.Box == "" {
return fmt.Errorf("image name is required")
}
Expand All @@ -107,7 +101,22 @@ func runPodConfigSetup(ctx context.Context, ex *sdk.Executor, c *spec.PodConfigS
if err := deploykit.RejectImageRefAsDeployName(c.Box); err != nil {
return err
}
return runConfig(ctx, ex, rt, c)
// Resolve the selection ONCE for the whole config run. A rootful target is explicit, and
// this plugin cannot finish one safely: the image store staging runs rootless and
// system-scope unit emission is unimplemented. Refuse with the remedy rather than silently
// deploying the rootless arm the operator did not ask for.
dc, err := loadDeploy(ctx, ex, "charly config")
if err != nil {
return err
}
sel, err := resolvePodSelectionFromConfig(dc, c.Box, c.Instance, rt)
if err != nil {
return err
}
if rootfulUnsupported(sel.Target, rt.RunMode) {
return rootfulRefusal(sel.Engine, rt.RunMode)
}
return runConfig(ctx, ex, rt, sel, c)
}

// resolveDeployRef mirrors the former BoxConfigSetupCmd.resolveDeployRef, resolved plugin-side
Expand Down Expand Up @@ -177,7 +186,7 @@ var mutateDeploy = func(ctx context.Context, ex *sdk.Executor, caller string, mu
}

//nolint:gocyclo // ported 1:1 from charly-core BoxConfigSetupCmd.runConfig — see the file header; splitting further would fragment the seam-call sequencing across files for no clarity gain
func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c *spec.PodConfigSetupRequest) error {
func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, sel podSelection, c *spec.PodConfigSetupRequest) error {
// Device detection runs plugin-side (detect_devices.go — the relocated
// "pod-config-detect-devices" seam, peer InvokeProvider verb:gpu). The former seam's call
// passed NO engine (only NoAutoDetect), so the ensure-cdi leg never fired at config time —
Expand All @@ -188,13 +197,13 @@ func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c
if err != nil {
return err
}
if err := ensureImagePresent(ctx, ex, imageRef, rt.RunEngine); err != nil {
if err := ensureImagePresent(ctx, ex, imageRef, sel.Target); err != nil {
return err
}
// ExtractMetadata PLUGIN-SIDE (#55 coneC-dsh — the pod-config-ensure-image host seam shrinks to
// the host-coupled ensureImagePresent leg; the deploykit label extract runs here, shedding the
// host leg's deploykit import).
metaPtr, err := deploykit.ExtractMetadata(rt.RunEngine, imageRef)
metaPtr, err := deploykit.ExtractMetadata(string(sel.Engine), imageRef)
if err != nil {
return err
}
Expand Down Expand Up @@ -255,7 +264,7 @@ func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c
// emission path below (the direct return and the quadlet write) and before any start, so the
// ownership is right however the container is created; `charly remove` keeps named volumes, so
// the re-run of an existing deploy is the normal case this must repair.
if err := reconcileNamedVolumeRoots(ctx, ex, rt.RunEngine, volumeRootKeepID(rt.RunMode, rt.RunEngine, len(bindMounts)), uid, gid, imageRef, volumes); err != nil {
if err := reconcileNamedVolumeRoots(ctx, ex, string(sel.Engine), volumeRootKeepID(rt.RunMode, string(sel.Engine), len(bindMounts)), uid, gid, imageRef, volumes); err != nil {
return err
}

Expand Down Expand Up @@ -337,17 +346,17 @@ func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c
}
envVars = appendAutoDetectedEnv(envVars, detected)

resolvedNetwork, netErr := kit.ResolveNetwork(network, rt.RunEngine)
resolvedNetwork, netErr := kit.ResolveNetwork(network, string(sel.Engine))
if netErr != nil {
return netErr
}

if conflicts := kit.CheckPortAvailability(ports, rt.BindAddress, rt.RunEngine); len(conflicts) > 0 {
if conflicts := kit.CheckPortAvailability(ports, rt.BindAddress, string(sel.Engine)); len(conflicts) > 0 {
fmt.Fprintf(os.Stderr, "Warning: port conflicts detected:%s", kit.FormatPortConflicts(conflicts, c.Box))
}

autoGen := c.Password == "auto"
provisioned, provFallback, provResolutions, isKeyring, perr := resolvePodProvisionSecrets(ctx, ex, &meta, c.Box, c.Instance, rt.RunEngine, autoGen, c.RefreshSecret)
provisioned, provFallback, provResolutions, isKeyring, perr := resolvePodProvisionSecrets(ctx, ex, &meta, c.Box, c.Instance, string(sel.Engine), autoGen, c.RefreshSecret)
if perr != nil {
return perr
}
Expand Down Expand Up @@ -379,7 +388,7 @@ func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c
var resolvedSidecars []deploykit.ResolvedSidecar
var deploySidecars map[string]json.RawMessage
if len(deploySidecarsRaw) > 0 {
scRes, err := resolvePodSidecars(ctx, ex, deploySidecarsRaw, deploykit.SidecarTemplatesOf(dc), c.Env, c.Box, c.Instance, rt.RunEngine, autoGen, c.RefreshSecret)
scRes, err := resolvePodSidecars(ctx, ex, deploySidecarsRaw, deploykit.SidecarTemplatesOf(dc), c.Env, c.Box, c.Instance, string(sel.Engine), autoGen, c.RefreshSecret)
if err != nil {
return scErr(err)
}
Expand Down Expand Up @@ -424,11 +433,11 @@ func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c
deploySaveState(ctx, ex, c.Box, c.Instance, saveInput)

if rt.RunMode == "direct" {
return runConfigDirect(rt, qcfg, bindMounts, resolvedSidecars, tunnelCfg)
return runConfigDirect(rt, sel, qcfg, bindMounts, resolvedSidecars, tunnelCfg)
}

if rt.RunMode == "systemd-unit" {
written, werr := writeSystemdUnitConfig(rt, qcfg, bindMounts, resolvedSidecars, tunnelCfg)
written, werr := writeSystemdUnitConfig(rt, sel, qcfg, bindMounts, resolvedSidecars, tunnelCfg)
if werr != nil {
return werr
}
Expand Down Expand Up @@ -550,7 +559,7 @@ func runConfig(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c
if err := start.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to start %s for post_enable hook: %v\n", svc, err)
} else {
engine := kit.EngineBinary(rt.RunEngine)
engine := sel.binary()
hookSecretEnv := resolvePodHookSecretEnv(ctx, ex, &meta, c.Box, c.Instance)
hookEnv := append(append([]string{}, c.Env...), hookSecretEnv...)
if err := runHook(engine, ctrName, hooks.PostEnable, hookEnv); err != nil {
Expand Down
54 changes: 30 additions & 24 deletions candy/plugin-deploy-pod/config_setup_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,17 +487,17 @@ func writeDirectDeployMarker(m directDeployMarker) error {
// meant a direct-mode deploy ignored the resolved engine (containerd/docker still ran podman).
// The engine is now a parameter, the argv comes from the engine class, and the caller executes
// that argv.
func directRunArgs(qcfg deploykit.QuadletConfig, bindMounts []deploykit.ResolvedBindMount, engine string, rootful bool) ([]string, error) {
func directRunArgs(qcfg deploykit.QuadletConfig, bindMounts []deploykit.ResolvedBindMount, sel podSelection) ([]string, error) {
name := kit.ContainerNameInstance(qcfg.BoxName, qcfg.Instance)
network := qcfg.Network
if network == "" {
network = "charly"
}
argv, _, err := engineRunArgv(engine, rootful, podEngineRun{
argv, _, err := engineRunArgv(sel.Target, podEngineRun{
Image: qcfg.ImageRef, Name: name, Network: network,
Hostname: name, Restart: "always",
Detach: true,
KeepID: engineKeepID(engine, len(bindMounts)),
KeepID: engineKeepID(string(sel.Engine), len(bindMounts)),
UID: qcfg.UID, GID: qcfg.GID,
Ports: localizedPorts(qcfg.Ports, qcfg.BindAddress),
Volumes: volumeArgs(qcfg.Volumes, bindMounts),
Expand All @@ -510,7 +510,7 @@ func directRunArgs(qcfg deploykit.QuadletConfig, bindMounts []deploykit.Resolved
}

// runConfigDirect mirrors the former BoxConfigSetupCmd.runConfigDirect, now engine-driven.
func runConfigDirect(rt *kit.ResolvedRuntime, qcfg deploykit.QuadletConfig, bindMounts []deploykit.ResolvedBindMount, sidecars []deploykit.ResolvedSidecar, tunnelCfg *spec.TunnelConfig) error {
func runConfigDirect(rt *kit.ResolvedRuntime, sel podSelection, qcfg deploykit.QuadletConfig, bindMounts []deploykit.ResolvedBindMount, sidecars []deploykit.ResolvedSidecar, tunnelCfg *spec.TunnelConfig) error {
if len(sidecars) > 0 {
fmt.Fprintf(os.Stderr, "Warning: sidecars are not supported in direct mode (skipping); use run_mode=quadlet for sidecar deploys.\n")
}
Expand All @@ -520,16 +520,16 @@ func runConfigDirect(rt *kit.ResolvedRuntime, qcfg deploykit.QuadletConfig, bind
if deploykit.HasEncryptedBindMounts(bindMounts) {
fmt.Fprintf(os.Stderr, "Warning: encrypted bind mounts require systemd-run; encrypted volumes will not be initialized in direct mode.\n")
}
if rootfulUnsupported(rt) {
return rootfulRefusal(rt.RunEngine, rt.RunMode)
if rootfulUnsupported(sel.Target, rt.RunMode) {
return rootfulRefusal(sel.Engine, rt.RunMode)
}
engine := rt.RunEngine
engine := string(sel.Engine)
name := kit.ContainerNameInstance(qcfg.BoxName, qcfg.Instance)
stop := engineStopArgv(engine, name)
stop := engineStopArgv(sel.Target, name)
_ = exec.Command(stop[0], stop[1:]...).Run()
rm := engineRemoveArgv(engine, name)
rm := engineRemoveArgv(sel.Target, name)
_ = exec.Command(rm[0], rm[1:]...).Run()
args, err := directRunArgs(qcfg, bindMounts, engine, podRootful(rt))
args, err := directRunArgs(qcfg, bindMounts, sel)
if err != nil {
return err
}
Expand Down Expand Up @@ -583,12 +583,11 @@ func podConfigWriteSelf(r spec.PodConfigWriteRequest) (spec.PodConfigWriteReply,
func provisionData(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntime, c *spec.PodConfigSetupRequest, meta *spec.BoxMetadata, imageRef string, dc *deploykit.DeployConfig, bindMounts []deploykit.ResolvedBindMount, volumes []deploykit.VolumeMount, deployVolumes []spec.DeployVolume) error {
dataMeta := meta
dataRef := imageRef
dataEngine := rt.RunEngine
if dc != nil {
if entry, ok := dc.Lookup(c.Box, c.Instance); ok && entry.Engine != "" {
dataEngine = string(entry.Engine)
}
sel, err := resolvePodSelectionFromConfig(dc, c.Box, c.Instance, rt)
if err != nil {
return err
}
dataEngine := string(sel.Engine)

if c.DataFrom != "" {
dataRef = c.DataFrom
Expand All @@ -597,7 +596,7 @@ func provisionData(ctx context.Context, ex *sdk.Executor, rt *kit.ResolvedRuntim
dataRef = resolved
}
}
if err := ensureImagePresent(ctx, ex, dataRef, rt.RunEngine); err != nil {
if err := ensureImagePresent(ctx, ex, dataRef, sel.Target); err != nil {
return fmt.Errorf("extracting metadata from data image %s: %w", dataRef, err)
}
dm, err := deploykit.ExtractMetadata(dataEngine, dataRef)
Expand Down Expand Up @@ -701,11 +700,18 @@ func updateAllDeployedQuadlets(ctx context.Context, ex *sdk.Executor, rt *kit.Re
imageRef = ref
}
}
if err := ensureImagePresent(ctx, ex, imageRef, rt.RunEngine); err != nil {
// Every entry resolves its OWN selection: a per-deploy `engine:`/`namespace:` in
// the overlay must reach that entry's quadlet, not the runtime's global engine.
updateSel, selErr := resolvePodSelectionFromConfig(dc, boxName, instance, rt)
if selErr != nil {
fmt.Fprintf(os.Stderr, "Warning: could not resolve the engine for %s, skipping quadlet update: %v\n", key, selErr)
continue
}
if err := ensureImagePresent(ctx, ex, imageRef, updateSel.Target); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not read metadata for %s, skipping quadlet update\n", key)
continue
}
metaPtr, err := deploykit.ExtractMetadata(rt.RunEngine, imageRef)
metaPtr, err := deploykit.ExtractMetadata(string(updateSel.Engine), imageRef)
if err != nil || metaPtr == nil {
continue
}
Expand All @@ -721,7 +727,7 @@ func updateAllDeployedQuadlets(ctx context.Context, ex *sdk.Executor, rt *kit.Re
continue
}
envVars = kit.EnrichNoProxy(envVars, deploykit.DeployedContainerNames(dc))
resolvedNetwork, _ := kit.ResolveNetwork(meta.Network, rt.RunEngine)
resolvedNetwork, _ := kit.ResolveNetwork(meta.Network, string(updateSel.Engine))

// Device detection runs plugin-side (detect_devices.go). The former seam call passed an
// empty request (NoAutoDetect=false, Engine="") and SWALLOWED the error — the
Expand Down Expand Up @@ -759,7 +765,7 @@ func updateAllDeployedQuadlets(ctx context.Context, ex *sdk.Executor, rt *kit.Re
}
envVars = appendAutoDetectedEnv(envVars, detected)

provisioned, _, provResolutions, isKeyring, perr := resolvePodProvisionSecrets(ctx, ex, &meta, boxName, instance, rt.RunEngine, true, nil)
provisioned, _, provResolutions, isKeyring, perr := resolvePodProvisionSecrets(ctx, ex, &meta, boxName, instance, string(updateSel.Engine), true, nil)
if perr != nil {
fmt.Fprintf(os.Stderr, "Warning: provisioning secrets for %s: %v\n", key, perr)
continue
Expand All @@ -786,7 +792,7 @@ func updateAllDeployedQuadlets(ctx context.Context, ex *sdk.Executor, rt *kit.Re
var resolvedSidecars []deploykit.ResolvedSidecar
podName := ""
if len(deploySidecarsRaw) > 0 {
scRes, err := resolvePodSidecars(ctx, ex, deploySidecarsRaw, deploykit.SidecarTemplatesOf(dc), nil, boxName, instance, rt.RunEngine, true, nil)
scRes, err := resolvePodSidecars(ctx, ex, deploySidecarsRaw, deploykit.SidecarTemplatesOf(dc), nil, boxName, instance, string(updateSel.Engine), true, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: resolving sidecars for %s: %v\n", key, err)
continue
Expand Down Expand Up @@ -1029,16 +1035,16 @@ func mergeVolumeConfigsLocal(base, overrides []spec.DeployVolume) []spec.DeployV
// through the engine `start_plan` body (engine_ops.go): the engine word selects the binary, the
// keep-id fact, the GPU flag style and the runtime handler, so a containerd/docker deploy no
// longer runs a podman argv. rootful selects the engine's mode-specific workload-user fact.
func buildStartArgs(engine string, rootful bool, imageRef string, uid, gid int, ports []string, name string, volumes []deploykit.VolumeMount, bindMounts []deploykit.ResolvedBindMount, gpu bool, bindAddr string, envVars []string, security spec.SecurityConfig, entrypoint []string, workingDir string, network ...string) ([]string, error) {
func buildStartArgs(sel podSelection, imageRef string, uid, gid int, ports []string, name string, volumes []deploykit.VolumeMount, bindMounts []deploykit.ResolvedBindMount, gpu bool, bindAddr string, envVars []string, security spec.SecurityConfig, entrypoint []string, workingDir string, network ...string) ([]string, error) {
net := ""
if len(network) > 0 {
net = network[0]
}
argv, _, err := engineRunArgv(engine, rootful, podEngineRun{
argv, _, err := engineRunArgv(sel.Target, podEngineRun{
Image: imageRef, Name: name, Network: net,
WorkingDir: workingDir, Hostname: name,
Detach: true, Remove: true,
KeepID: engineKeepID(engine, len(bindMounts)),
KeepID: engineKeepID(string(sel.Engine), len(bindMounts)),
UID: uid, GID: gid,
Ports: localizedPorts(ports, bindAddr),
Volumes: volumeArgs(volumes, bindMounts),
Expand Down
6 changes: 5 additions & 1 deletion candy/plugin-deploy-pod/enc_tunnel_resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ func resolvePodTunnelPlan(dc *deploykit.DeployConfig, box, instance string) *spe
if err != nil {
return nil
}
engine := rt.RunEngine
sel, err := resolvePodSelectionFromConfig(dc, box, instance, rt)
if err != nil {
return nil
}
engine := string(sel.Engine)
ctrName := kit.ContainerNameInstance(box, instance)
imageRef := kit.ContainerImage(engine, ctrName)
if imageRef == "" {
Expand Down
Loading