Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Implement force for manual releases
Browse files Browse the repository at this point in the history
A manual release through the API or then with fluxctl may come across
cases where configuration prevents it to perform a certain update. That
can either be a locked controller, or a container image that is
filtered. This PR adds `--force` to the CLI and `Force` to the release
spec in the API to circumvent restrictions on a per-command basis.

For locked controllers, `--force` will disregard their locked state when
controllers are listed separeately. But `--all` invocation will ignore
the `--force` flag.

For container tag filters, `--force` will allow to update to images
outside of that specification. Again, using `--update-all-images` will
remove the effect of `--force` for images.

For older daemons used in conjunction with a new fluxctl, the `--force`
param will just be ignored which should be OK since it's doing less than
intended.
  • Loading branch information
rndstr committed Aug 13, 2018
1 parent 65e961e commit bd4ea9e
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 10 deletions.
13 changes: 12 additions & 1 deletion cmd/fluxctl/release_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type controllerReleaseOpts struct {
exclude []string
dryRun bool
interactive bool
force bool
outputOpts
cause update.Cause

Expand Down Expand Up @@ -55,6 +56,7 @@ func (opts *controllerReleaseOpts) Command() *cobra.Command {
cmd.Flags().StringSliceVar(&opts.exclude, "exclude", []string{}, "List of controllers to exclude")
cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "Do not release anything; just report back what would have been done")
cmd.Flags().BoolVar(&opts.interactive, "interactive", false, "Select interactively which containers to update")
cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Disregard locks and container image filters (has no effect when used with --all or --update-all-images)")

// Deprecated
cmd.Flags().StringSliceVarP(&opts.services, "service", "s", []string{}, "Service to release")
Expand All @@ -76,8 +78,16 @@ func (opts *controllerReleaseOpts) RunE(cmd *cobra.Command, args []string) error
return err
}

if len(opts.controllers) <= 0 && !opts.allControllers {

switch true {
case len(opts.controllers) <= 0 && !opts.allControllers:
return newUsageError("please supply either --all, or at least one --controller=<controller>")
case opts.force && opts.allControllers && opts.allImages:
return newUsageError("--force has no effect when used with --all and --update-all-images")
case opts.force && opts.allControllers:
fmt.Fprintf(cmd.OutOrStderr(), "Warning: --force will not ignore locked controllers when used with --all\n")
case opts.force && opts.allImages:
fmt.Fprintf(cmd.OutOrStderr(), "Warning: --force will not ignore container image tags when used with --update-all-images\n")
}

var controllers []update.ResourceSpec
Expand Down Expand Up @@ -133,6 +143,7 @@ func (opts *controllerReleaseOpts) RunE(cmd *cobra.Command, args []string) error
ImageSpec: image,
Kind: kind,
Excludes: excludes,
Force: opts.force,
}
jobID, err := opts.API.UpdateManifests(ctx, update.Spec{
Type: update.Images,
Expand Down
244 changes: 243 additions & 1 deletion release/releaser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ var (
},
}

semverHwImg = "quay.io/weaveworks/helloworld:3.0.0"
semverHwRef, _ = image.ParseRef(semverHwImg)
semverSvcID = flux.MustParseResourceID("default:deployment/semver")
semverSvc = cluster.Controller{
ID: semverSvcID,
Containers: cluster.ContainersOrExcuse{
Containers: []resource.Container{
{
Name: helloContainer,
Image: oldRef,
},
},
},
}
semverSvcSpec, _ = update.ParseResourceSpec(semverSvc.ID.String())

testSvcID = flux.MustParseResourceID("default:deployment/test-service")
testSvc = cluster.Controller{
ID: testSvcID,
Expand All @@ -95,10 +111,15 @@ var (
// this is what we store in the registry cache
canonSidecarRef, _ = image.ParseRef("index.docker.io/weaveworks/sidecar:master-a000002")

timeNow = time.Now()
timeNow = time.Now()
timePast = timeNow.Add(-1 * time.Minute)

mockRegistry = &registryMock.Registry{
Images: []image.Info{
{
ID: semverHwRef,
CreatedAt: timePast,
},
{
ID: newHwRef,
CreatedAt: timeNow,
Expand Down Expand Up @@ -154,6 +175,11 @@ var ignoredNotInCluster = update.ControllerResult{
Error: update.NotInCluster,
}

var skippedLocked = update.ControllerResult{
Status: update.ReleaseStatusSkipped,
Error: update.Locked,
}

var skippedNotInCluster = update.ControllerResult{
Status: update.ReleaseStatusSkipped,
Error: update.NotInCluster,
Expand Down Expand Up @@ -368,6 +394,222 @@ func Test_FilterLogic(t *testing.T) {
}
}

func Test_Force_lockedController(t *testing.T) {
cluster := mockCluster(lockedSvc)
success := update.ControllerResult{
Status: update.ReleaseStatusSuccess,
PerContainer: []update.ContainerUpdate{
{
Container: lockedContainer,
Current: oldLockedRef,
Target: newLockedRef,
},
},
}
for _, tst := range []struct {
Name string
Spec update.ReleaseSpec
Expected update.Result
}{
{
Name: "force ignores service lock (--controller --update-image)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{lockedSvcSpec},
ImageSpec: update.ImageSpecFromRef(newLockedRef),
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/locked-service"): success,
flux.MustParseResourceID("default:deployment/helloworld"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/test-service"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/multi-deploy"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/list-deploy"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/semver"): ignoredNotIncluded,
},
},
{
Name: "force does not ignore lock if updating all controllers (--all --update-image)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{update.ResourceSpecAll},
ImageSpec: update.ImageSpecFromRef(newLockedRef),
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/locked-service"): skippedLocked,
flux.MustParseResourceID("default:deployment/helloworld"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/test-service"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/multi-deploy"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/list-deploy"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/semver"): skippedNotInCluster,
},
},
{
Name: "force ignores service lock (--controller --update-all-images)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{lockedSvcSpec},
ImageSpec: update.ImageSpecLatest,
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/locked-service"): success,
flux.MustParseResourceID("default:deployment/helloworld"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/test-service"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/multi-deploy"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/list-deploy"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/semver"): ignoredNotIncluded,
},
},
{
Name: "force does not ignore lock if updating all controllers (--all --update-all-images)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{update.ResourceSpecAll},
ImageSpec: update.ImageSpecLatest,
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/locked-service"): skippedLocked,
flux.MustParseResourceID("default:deployment/helloworld"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/test-service"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/multi-deploy"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/list-deploy"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/semver"): skippedNotInCluster,
},
},
} {
t.Run(tst.Name, func(t *testing.T) {
checkout, cleanup := setup(t)
defer cleanup()
testRelease(t, &ReleaseContext{
cluster: cluster,
manifests: mockManifests,
registry: mockRegistry,
repo: checkout,
}, tst.Spec, tst.Expected)
})
}
}

func Test_Force_filteredContainer(t *testing.T) {
cluster := mockCluster(semverSvc)
successNew := update.ControllerResult{
Status: update.ReleaseStatusSuccess,
PerContainer: []update.ContainerUpdate{
{
Container: helloContainer,
Current: oldRef,
Target: newHwRef,
},
},
}
successSemver := update.ControllerResult{
Status: update.ReleaseStatusSuccess,
PerContainer: []update.ContainerUpdate{
{
Container: helloContainer,
Current: oldRef,
Target: semverHwRef,
},
},
}
for _, tst := range []struct {
Name string
Spec update.ReleaseSpec
Expected update.Result
}{
{
Name: "force ignores container tag pattern (--controller --update-image)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{semverSvcSpec},
ImageSpec: update.ImageSpecFromRef(newHwRef), // does not match filter
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/semver"): successNew,
flux.MustParseResourceID("default:deployment/locked-service"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/helloworld"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/test-service"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/multi-deploy"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/list-deploy"): ignoredNotIncluded,
},
},
{
Name: "force ignores container tag pattern (--all --update-image)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{update.ResourceSpecAll},
ImageSpec: update.ImageSpecFromRef(newHwRef), // does not match filter
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/semver"): successNew,
flux.MustParseResourceID("default:deployment/locked-service"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/helloworld"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/test-service"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/multi-deploy"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/list-deploy"): skippedNotInCluster,
},
},
{
Name: "force complies with semver when updating all images (--controller --update-all-image)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{semverSvcSpec},
ImageSpec: update.ImageSpecLatest, // will filter images by semver and pick newest version
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/semver"): successSemver,
flux.MustParseResourceID("default:deployment/locked-service"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/helloworld"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/test-service"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/multi-deploy"): ignoredNotIncluded,
flux.MustParseResourceID("default:deployment/list-deploy"): ignoredNotIncluded,
},
},
{
Name: "force complies with semver when updating all images (--all --update-all-image)",
Spec: update.ReleaseSpec{
ServiceSpecs: []update.ResourceSpec{update.ResourceSpecAll},
ImageSpec: update.ImageSpecLatest,
Kind: update.ReleaseKindExecute,
Excludes: []flux.ResourceID{},
Force: true,
},
Expected: update.Result{
flux.MustParseResourceID("default:deployment/semver"): successSemver,
flux.MustParseResourceID("default:deployment/locked-service"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/helloworld"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/test-service"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/multi-deploy"): skippedNotInCluster,
flux.MustParseResourceID("default:deployment/list-deploy"): skippedNotInCluster,
},
},
} {
t.Run(tst.Name, func(t *testing.T) {
checkout, cleanup := setup(t)
defer cleanup()
testRelease(t, &ReleaseContext{
cluster: cluster,
manifests: mockManifests,
registry: mockRegistry,
repo: checkout,
}, tst.Spec, tst.Expected)
})
}
}

func Test_ImageStatus(t *testing.T) {
cluster := mockCluster(hwSvc, lockedSvc, testSvc)
upToDateRegistry := &registryMock.Registry{
Expand Down
24 changes: 16 additions & 8 deletions update/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,16 @@ func (s ReleaseSpec) filters(rc ReleaseContext) ([]ControllerFilter, []Controlle
postfilters = append(postfilters, &SpecificImageFilter{id})
}

// Locked filter
services, err := rc.ServicesWithPolicies()
if err != nil {
return nil, nil, err
// Filter out locked controllers unless given a specific controller(s) and forced
if !(len(ids) > 0 && s.Force) {
// Locked filter
services, err := rc.ServicesWithPolicies()
if err != nil {
return nil, nil, err
}
lockedSet := services.OnlyWithPolicy(policy.Locked)
postfilters = append(postfilters, &LockedFilter{lockedSet.ToSlice()})
}
lockedSet := services.OnlyWithPolicy(policy.Locked)
postfilters = append(postfilters, &LockedFilter{lockedSet.ToSlice()})

return prefilters, postfilters, nil
}
Expand Down Expand Up @@ -232,8 +235,13 @@ func (s ReleaseSpec) calculateImageUpdates(rc ReleaseContext, candidates []*Cont
currentImageID := container.Image

tagPattern := policy.PatternAll
if pattern, ok := u.Resource.Policy().Get(policy.TagPrefix(container.Name)); ok {
tagPattern = policy.NewPattern(pattern)
// Use the container's filter if the spec does not want to force release, or
// all images requested
if !s.Force || s.ImageSpec == ImageSpecLatest {
// Use the container's filter since we are not forcing
if pattern, ok := u.Resource.Policy().Get(policy.TagPrefix(container.Name)); ok {
tagPattern = policy.NewPattern(pattern)
}
}

filteredImages := imageRepos.GetRepoImages(currentImageID.Name).FilterAndSort(tagPattern)
Expand Down

0 comments on commit bd4ea9e

Please sign in to comment.