Skip to content

Commit

Permalink
Implements replace strategy (#116)
Browse files Browse the repository at this point in the history
* Implement strategy.spinnaker.io/replace

* Adds tests for Replace

* Fixes linter issue

* Implements strategy.spinnaker.io/replace
  • Loading branch information
dmrogers7 authored Jan 25, 2022
1 parent 423b3c9 commit 0e10238
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 7 deletions.
24 changes: 18 additions & 6 deletions internal/api/core/kubernetes/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,25 @@ func (cc *Controller) Deploy(c *gin.Context, dm DeployManifestRequest) {
}
}

meta, err := provider.Client.Apply(&manifest)
if err != nil {
e := fmt.Errorf("error applying manifest (kind: %s, apiVersion: %s, name: %s): %s",
manifest.GetKind(), manifest.GroupVersionKind().Version, manifest.GetName(), err.Error())
clouddriver.Error(c, http.StatusInternalServerError, e)
meta := kubernetes.Metadata{}
if kubernetes.Replace(manifest) {
meta, err = provider.Client.Replace(&manifest)
if err != nil {
e := fmt.Errorf("error replacing manifest (kind: %s, apiVersion: %s, name: %s): %s",
manifest.GetKind(), manifest.GroupVersionKind().Version, manifest.GetName(), err.Error())
clouddriver.Error(c, http.StatusInternalServerError, e)

return
return
}
} else {
meta, err = provider.Client.Apply(&manifest)
if err != nil {
e := fmt.Errorf("error applying manifest (kind: %s, apiVersion: %s, name: %s): %s",
manifest.GetKind(), manifest.GroupVersionKind().Version, manifest.GetName(), err.Error())
clouddriver.Error(c, http.StatusInternalServerError, e)

return
}
}

kr := kubernetes.Resource{
Expand Down
36 changes: 36 additions & 0 deletions internal/api/core/kubernetes/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,42 @@ var _ = Describe("Deploy", func() {
})
})

When("the manifest uses replace strategy", func() {
BeforeEach(func() {
deployManifestRequest = DeployManifestRequest{
Manifests: []map[string]interface{}{
{
"kind": "Job",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"strategy.spinnaker.io/replace": "true",
},
"name": "test-name",
"namespace": "test-namespace",
},
},
},
}
})

When("replace returns an error", func() {
BeforeEach(func() {
fakeKubeClient.ReplaceReturns(kubernetes.Metadata{}, errors.New("ReplaceReturns fake error"))
})

It("returns an error", func() {
Expect(c.Writer.Status()).To(Equal(http.StatusInternalServerError))
Expect(c.Errors.Last().Error()).To(Equal("error replacing manifest (kind: Job, apiVersion: v1, name: test-name): ReplaceReturns fake error"))
})
})

It("it succeeds, calling replace", func() {
Expect(c.Writer.Status()).To(Equal(http.StatusOK))
Expect(fakeKubeClient.ReplaceCallCount()).To(Equal(1))
})
})

Context("when the manifest uses Spinnaker managed traffic", func() {
BeforeEach(func() {
deployManifestRequest = DeployManifestRequest{
Expand Down
8 changes: 7 additions & 1 deletion internal/api/core/kubernetes/runjob.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ func (cc *Controller) RunJob(c *gin.Context, rj RunJobRequest) {

kubernetes.BindArtifacts(&u, append(rj.RequiredArtifacts, rj.OptionalArtifacts...))

meta, err := provider.Client.Apply(&u)
meta := kubernetes.Metadata{}
if kubernetes.Replace(u) {
meta, err = provider.Client.Replace(&u)
} else {
meta, err = provider.Client.Apply(&u)
}

if err != nil {
clouddriver.Error(c, http.StatusInternalServerError, err)
return
Expand Down
32 changes: 32 additions & 0 deletions internal/api/core/kubernetes/runjob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,38 @@ var _ = Describe("RunJob", func() {
})
})

When("the manifest uses replace strategy", func() {
BeforeEach(func() {
runJobRequest.Manifest = map[string]interface{}{
"kind": "Job",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"strategy.spinnaker.io/replace": "true",
},
"name": "test-name",
"namespace": "test-namespace",
},
}
})

When("replace returns an error", func() {
BeforeEach(func() {
fakeKubeClient.ReplaceReturns(kubernetes.Metadata{}, errors.New("ReplaceReturns fake error"))
})

It("returns an error", func() {
Expect(c.Writer.Status()).To(Equal(http.StatusInternalServerError))
Expect(c.Errors.Last().Error()).To(Equal("ReplaceReturns fake error"))
})
})

It("it succeeds, calling replace", func() {
Expect(c.Writer.Status()).To(Equal(http.StatusOK))
Expect(fakeKubeClient.ReplaceCallCount()).To(Equal(1))
})
})

Context("annotating 'artifact.spinnaker.io/location'", func() {
When("the namespace is not set", func() {
BeforeEach(func() {
Expand Down
76 changes: 76 additions & 0 deletions internal/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Metadata struct {
//go:generate counterfeiter . Client
type Client interface {
Apply(*unstructured.Unstructured) (Metadata, error)
Replace(*unstructured.Unstructured) (Metadata, error)
DeleteResourceByKindAndNameAndNamespace(string, string, string, metav1.DeleteOptions) error
Discover() error
GVRForKind(string) (schema.GroupVersionResource, error)
Expand Down Expand Up @@ -136,6 +137,81 @@ func (c *client) Apply(u *unstructured.Unstructured) (Metadata, error) {
return metadata, nil
}

// Replace a given manifest.
func (c *client) Replace(u *unstructured.Unstructured) (Metadata, error) {
metadata := Metadata{}
gvk := u.GroupVersionKind()

restMapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return metadata, err
}

gvr := restMapping.Resource
gv := gvk.GroupVersion()
c.config.GroupVersion = &gv

restClient, err := newRestClient(*c.config, gv)
if err != nil {
return metadata, err
}

helper := resource.NewHelper(restClient, restMapping)

info := &resource.Info{
Client: restClient,
Mapping: restMapping,
Namespace: u.GetNamespace(),
Name: u.GetName(),
Source: "",
Object: u,
ResourceVersion: restMapping.Resource.Version,
}

// If annotation kubectl.kubernetes.io/last-applied-configuration exists, then update it.
err = util.CreateOrUpdateAnnotation(false, info.Object, unstructured.UnstructuredJSONScheme)
if err != nil {
return metadata, err
}

exists := true
// Determine if the resource currently exists.
if err := info.Get(); err != nil {
if !errors.IsNotFound(err) {
return metadata, err
}

exists = false
}

if !exists {
// Create the resource if it doesn't exist.
obj, err := helper.Create(info.Namespace, true, info.Object)
if err != nil {
return metadata, err
}

_ = info.Refresh(obj, true)
} else {
// Replace the resource if it does exist.
obj, err := helper.Replace(info.Namespace, info.Name, true, info.Object)
if err != nil {
return metadata, err
}

_ = info.Refresh(obj, true)
}

metadata.Name = u.GetName()
metadata.Namespace = u.GetNamespace()
metadata.Group = gvr.Group
metadata.Resource = gvr.Resource
metadata.Kind = gvk.Kind
metadata.Version = gvr.Version

return metadata, nil
}

func newRestClient(restConfig rest.Config, gv schema.GroupVersion) (rest.Interface, error) {
restConfig.ContentConfig = resource.UnstructuredPlusDefaultContentConfig()
restConfig.GroupVersion = &gv
Expand Down
79 changes: 79 additions & 0 deletions internal/kubernetes/kubernetesfakes/fake_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0e10238

Please sign in to comment.