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

Commit

Permalink
Add containers release specs to update-manifests
Browse files Browse the repository at this point in the history
This adds a new release spec format providing update requests on container
level.

Sample POST request body:
```
{
  "type": "containers",
  "cause": {
    "Message": "sample request body",
    "User": "alice"
  },
  "spec": {
    "ContainerSpecs": {
      "default:deployment/nginx": [
        {
          "Container": "nginx",
          "Current": "nginx:1.14.0",
          "Target": "nginx:1.15.0"
        }
      ],
      "default:deployment/helloworld": [
        {
          "Container": "helloworld",
          "Current": "quay.io/weaveworks/helloworld:master-07a1b6b",
          "Target": "quay.io/weaveworks/helloworld:master-a000004"
        },
        {
          "Container": "sidecar",
          "Current": "quay.io/weaveworks/sidecar:master-a000001",
          "Target": "quay.io/weaveworks/sidecar:master-a000002"
        }
      ]
    },
    "Kind": "plan",
    "IgnoreFailedControllers": false
  }
}
```

The request will fail if any of the `Current` container image requirements
are not met. To have partial updates go through and ignore the failed
requirements, one can pass `true` for `IgnoreFailedControllers`.
  • Loading branch information
rndstr committed Jul 9, 2018
1 parent 608d518 commit e160b8a
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 19 deletions.
2 changes: 2 additions & 0 deletions release/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ func (rc *ReleaseContext) SelectServices(results update.Result, prefilters, post
return filteredUpdates, nil
}

// WorkloadsForUpdate collects all workloads defined in manifests and prepares a list of
// controller updates for each of them. It does not consider updatability.
func (rc *ReleaseContext) WorkloadsForUpdate() (map[flux.ResourceID]*update.ControllerUpdate, error) {
resources, err := rc.manifests.LoadManifests(rc.repo.Dir(), rc.repo.ManifestDir())
if err != nil {
Expand Down
165 changes: 159 additions & 6 deletions release/releaser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package release

import (
"encoding/json"
"fmt"
"reflect"
"testing"
"time"

"github.com/go-kit/kit/log"

"github.com/stretchr/testify/assert"
"github.com/weaveworks/flux"
"github.com/weaveworks/flux/cluster"
"github.com/weaveworks/flux/cluster/kubernetes"
Expand All @@ -23,6 +25,8 @@ var (
// This must match the value in cluster/kubernetes/testfiles/data.go
helloContainer = "greeter"
sidecarContainer = "sidecar"
lockedContainer = "locked-service"
testContainer = "test-service"

oldImage = "quay.io/weaveworks/helloworld:master-a000001"
oldRef, _ = image.ParseRef(oldImage)
Expand Down Expand Up @@ -52,27 +56,28 @@ var (
oldLockedRef, _ = image.ParseRef(oldLockedImg)

newLockedImg = "quay.io/weaveworks/locked-service:2"
newLockedID, _ = image.ParseRef(newLockedImg)
newLockedRef, _ = image.ParseRef(newLockedImg)
lockedSvcID, _ = flux.ParseResourceID("default:deployment/locked-service")
lockedSvcSpec, _ = update.ParseResourceSpec(lockedSvcID.String())
lockedSvc = cluster.Controller{
ID: lockedSvcID,
Containers: cluster.ContainersOrExcuse{
Containers: []resource.Container{
{
Name: "locked-service",
Name: lockedContainer,
Image: oldLockedRef,
},
},
},
}

testSvc = cluster.Controller{
ID: flux.MustParseResourceID("default:deployment/test-service"),
testSvcID = flux.MustParseResourceID("default:deployment/test-service")
testSvc = cluster.Controller{
ID: testSvcID,
Containers: cluster.ContainersOrExcuse{
Containers: []resource.Container{
{
Name: "test-service",
Name: testContainer,
Image: testServiceRef,
},
},
Expand Down Expand Up @@ -104,7 +109,7 @@ var (
CreatedAt: timeNow,
},
{
ID: newLockedID,
ID: newLockedRef,
CreatedAt: timeNow,
},
},
Expand Down Expand Up @@ -527,6 +532,154 @@ func Test_UpdateList(t *testing.T) {
}
}

func Test_UpdateContainers_someFail(t *testing.T) {
cluster := mockCluster(hwSvc, testSvc)
checkout, cleanup := setup(t)
defer cleanup()

ctx := &ReleaseContext{
cluster: cluster,
manifests: mockManifests,
repo: checkout,
registry: mockRegistry,
}
spec := update.ContainerSpecs{
ContainerSpecs: map[flux.ResourceID][]update.ContainerUpdate{
hwSvcID: { // success
{
Container: helloContainer,
Current: oldRef,
Target: newHwRef,
},
},
testSvcID: { // fail
{
Container: testContainer,
Current: oldRef,
Target: oldRef,
},
},
},
Kind: update.ReleaseKindExecute,
}

_, err := Release(ctx, spec, log.NewNopLogger())
assert.Error(t, err)

spec.IgnoreFailedControllers = true
results, err := Release(ctx, spec, log.NewNopLogger())
assert.NoError(t, err)

expected := update.Result{
hwSvcID: update.ControllerResult{
Status: update.ReleaseStatusSuccess,
PerContainer: []update.ContainerUpdate{{
Container: helloContainer,
Current: oldRef,
Target: newHwRef,
}},
},
testSvcID: update.ControllerResult{
Status: update.ReleaseStatusFailed,
Error: fmt.Sprintf(update.ContainerTagMismatch, testContainer),
},
}
assert.Equal(t, expected[hwSvcID], results[hwSvcID])
assert.Equal(t, expected[testSvcID], results[testSvcID])
}

func Test_UpdateContainers(t *testing.T) {
cluster := mockCluster(hwSvc, lockedSvc)
checkout, cleanup := setup(t)
defer cleanup()
ctx := &ReleaseContext{
cluster: cluster,
manifests: mockManifests,
repo: checkout,
registry: mockRegistry,
}
for _, tst := range []struct {
Name string
Spec []update.ContainerUpdate
Expected update.ControllerResult
Commit string
}{
{
Name: "multiple containers",
Spec: []update.ContainerUpdate{
{
Container: helloContainer,
Current: oldRef,
Target: newHwRef,
},
{
Container: sidecarContainer,
Current: sidecarRef,
Target: newSidecarRef,
},
},
Expected: update.ControllerResult{
Status: update.ReleaseStatusSuccess,
PerContainer: []update.ContainerUpdate{{
Container: helloContainer,
Current: oldRef,
Target: newHwRef,
}, {
Container: sidecarContainer,
Current: sidecarRef,
Target: newSidecarRef,
}},
},
Commit: "Release containers\n\ndefault:deployment/helloworld\n- quay.io/weaveworks/helloworld:master-a000002\n- weaveworks/sidecar:master-a000002\n",
},
{
Name: "container tag mismatch",
Spec: []update.ContainerUpdate{
{
Container: helloContainer,
Current: newHwRef, // mismatch
Target: oldRef,
},
{
Container: sidecarContainer,
Current: sidecarRef,
Target: newSidecarRef,
},
},
Expected: update.ControllerResult{
Status: update.ReleaseStatusFailed,
Error: fmt.Sprintf(update.ContainerTagMismatch, helloContainer),
},
Commit: "Release containers\n\ndefault:deployment/helloworld failed: container tag mismatch: greeter",
},
{
Name: "container not found",
Spec: []update.ContainerUpdate{
{
Container: "foo",
Current: oldRef,
Target: newHwRef,
},
},
Expected: update.ControllerResult{
Status: update.ReleaseStatusFailed,
Error: fmt.Sprintf(update.ContainerNotFound, "foo"),
},
Commit: "Release containers\n\ndefault:deployment/helloworld failed: container not found: foo",
},
} {
specs := update.ContainerSpecs{
ContainerSpecs: map[flux.ResourceID][]update.ContainerUpdate{hwSvcID: tst.Spec},
Kind: update.ReleaseKindExecute,
IgnoreFailedControllers: true,
}
results, err := Release(ctx, specs, log.NewNopLogger())
assert.NoError(t, err, tst.Name)
assert.Equal(t, tst.Expected, results[hwSvcID], tst.Name)
assert.Equal(t, tst.Commit, specs.CommitMessage(results), tst.Name)
}
}

func testRelease(t *testing.T, ctx *ReleaseContext, spec update.ReleaseSpec, expected update.Result) {
results, err := Release(ctx, spec, log.NewNopLogger())
if err != nil {
Expand Down
120 changes: 120 additions & 0 deletions update/containers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package update

import (
"bytes"
"errors"
"fmt"

"github.com/go-kit/kit/log"

"github.com/weaveworks/flux"
"github.com/weaveworks/flux/resource"
)

// ContainerSpecs defines the spec for a `containers` manifest update.
type ContainerSpecs struct {
Kind ReleaseKind
ContainerSpecs map[flux.ResourceID][]ContainerUpdate
IgnoreFailedControllers bool
}

var errCannotSatisfySpecs = errors.New("cannot satisfy specs")

// CalculateRelease computes required controller updates to satisfy this specification.
// It returns an error if any spec calculation fails unless `IgnoreFailedControllers` is true.
func (s ContainerSpecs) CalculateRelease(rc ReleaseContext, logger log.Logger) ([]*ControllerUpdate, Result, error) {
results := Result{}

// Collect data from services in spec
var rids []flux.ResourceID
for rid := range s.ContainerSpecs {
rids = append(rids, rid)
}
all, err := rc.SelectServices(results, []ControllerFilter{&IncludeFilter{IDs: rids}}, nil)
if err != nil {
return nil, results, err
}

// Look at all controllers of services
var updates []*ControllerUpdate
for _, u := range all {
cs, err := u.Controller.ContainersOrError()
if err != nil {
results[u.ResourceID] = ControllerResult{
Status: ReleaseStatusFailed,
Error: err.Error(),
}
continue
}

containers := map[string]resource.Container{}
for _, spec := range cs {
containers[spec.Name] = spec
}

// Go through specs and collect updates
var containerUpdates []ContainerUpdate
for _, spec := range s.ContainerSpecs[u.ResourceID] {
container, ok := containers[spec.Container]
if !ok {
results[u.ResourceID] = ControllerResult{
Status: ReleaseStatusFailed,
Error: fmt.Sprintf(ContainerNotFound, spec.Container),
}
break // go to next controller
}

if container.Image != spec.Current {
results[u.ResourceID] = ControllerResult{
Status: ReleaseStatusFailed,
Error: fmt.Sprintf(ContainerTagMismatch, spec.Container),
}
break // go to next controller
}
containerUpdates = append(containerUpdates, spec)
}

if res := results[u.ResourceID]; res.Status == "" {
u.Updates = containerUpdates
updates = append(updates, u)
results[u.ResourceID] = ControllerResult{
Status: ReleaseStatusSuccess,
PerContainer: u.Updates,
}
}
}

if !s.IgnoreFailedControllers {
for _, res := range results {
if res.Status == ReleaseStatusFailed {
return updates, results, errCannotSatisfySpecs
}
}
}

return updates, results, nil
}

func (s ContainerSpecs) ReleaseKind() ReleaseKind {
return s.Kind
}

func (s ContainerSpecs) ReleaseType() ReleaseType {
return "containers"
}

func (s ContainerSpecs) CommitMessage(result Result) string {
buf := &bytes.Buffer{}
fmt.Fprintln(buf, "Release containers")
for _, res := range result.AffectedResources() {
fmt.Fprintf(buf, "\n%s", res)
for _, upd := range result[res].PerContainer {
fmt.Fprintf(buf, "\n- %s", upd.Target)
}
fmt.Fprintln(buf)
}
if err := result.Error(); err != "" {
fmt.Fprintf(buf, "\n%s", result.Error())
}
return buf.String()
}
20 changes: 11 additions & 9 deletions update/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import (
)

const (
Locked = "locked"
NotIncluded = "not included"
Excluded = "excluded"
DifferentImage = "a different image"
NotInCluster = "not running in cluster"
NotInRepo = "not found in repository"
ImageNotFound = "cannot find one or more images"
ImageUpToDate = "image(s) up to date"
DoesNotUseImage = "does not use image(s)"
Locked = "locked"
NotIncluded = "not included"
Excluded = "excluded"
DifferentImage = "a different image"
NotInCluster = "not running in cluster"
NotInRepo = "not found in repository"
ImageNotFound = "cannot find one or more images"
ImageUpToDate = "image(s) up to date"
DoesNotUseImage = "does not use image(s)"
ContainerNotFound = "container not found: %s"
ContainerTagMismatch = "container tag mismatch: %s"
)

type SpecificImageFilter struct {
Expand Down
Loading

0 comments on commit e160b8a

Please sign in to comment.