diff --git a/internal/api/core/kubernetes/deploy.go b/internal/api/core/kubernetes/deploy.go index 8bb3adc..9dd69c3 100644 --- a/internal/api/core/kubernetes/deploy.go +++ b/internal/api/core/kubernetes/deploy.go @@ -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{ diff --git a/internal/api/core/kubernetes/deploy_test.go b/internal/api/core/kubernetes/deploy_test.go index a004130..7da919d 100644 --- a/internal/api/core/kubernetes/deploy_test.go +++ b/internal/api/core/kubernetes/deploy_test.go @@ -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{ diff --git a/internal/api/core/kubernetes/runjob.go b/internal/api/core/kubernetes/runjob.go index 8cb364b..f080593 100644 --- a/internal/api/core/kubernetes/runjob.go +++ b/internal/api/core/kubernetes/runjob.go @@ -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 diff --git a/internal/api/core/kubernetes/runjob_test.go b/internal/api/core/kubernetes/runjob_test.go index 0502206..208b6ca 100644 --- a/internal/api/core/kubernetes/runjob_test.go +++ b/internal/api/core/kubernetes/runjob_test.go @@ -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() { diff --git a/internal/kubernetes/client.go b/internal/kubernetes/client.go index 31e16a3..f940c95 100644 --- a/internal/kubernetes/client.go +++ b/internal/kubernetes/client.go @@ -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) @@ -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 diff --git a/internal/kubernetes/kubernetesfakes/fake_client.go b/internal/kubernetes/kubernetesfakes/fake_client.go index 03a3a5e..a96dad8 100644 --- a/internal/kubernetes/kubernetesfakes/fake_client.go +++ b/internal/kubernetes/kubernetesfakes/fake_client.go @@ -204,6 +204,19 @@ type FakeClient struct { result2 *unstructured.Unstructured result3 error } + ReplaceStub func(*unstructured.Unstructured) (kubernetes.Metadata, error) + replaceMutex sync.RWMutex + replaceArgsForCall []struct { + arg1 *unstructured.Unstructured + } + replaceReturns struct { + result1 kubernetes.Metadata + result2 error + } + replaceReturnsOnCall map[int]struct { + result1 kubernetes.Metadata + result2 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -1065,6 +1078,70 @@ func (fake *FakeClient) PatchUsingStrategyReturnsOnCall(i int, result1 kubernete }{result1, result2, result3} } +func (fake *FakeClient) Replace(arg1 *unstructured.Unstructured) (kubernetes.Metadata, error) { + fake.replaceMutex.Lock() + ret, specificReturn := fake.replaceReturnsOnCall[len(fake.replaceArgsForCall)] + fake.replaceArgsForCall = append(fake.replaceArgsForCall, struct { + arg1 *unstructured.Unstructured + }{arg1}) + stub := fake.ReplaceStub + fakeReturns := fake.replaceReturns + fake.recordInvocation("Replace", []interface{}{arg1}) + fake.replaceMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeClient) ReplaceCallCount() int { + fake.replaceMutex.RLock() + defer fake.replaceMutex.RUnlock() + return len(fake.replaceArgsForCall) +} + +func (fake *FakeClient) ReplaceCalls(stub func(*unstructured.Unstructured) (kubernetes.Metadata, error)) { + fake.replaceMutex.Lock() + defer fake.replaceMutex.Unlock() + fake.ReplaceStub = stub +} + +func (fake *FakeClient) ReplaceArgsForCall(i int) *unstructured.Unstructured { + fake.replaceMutex.RLock() + defer fake.replaceMutex.RUnlock() + argsForCall := fake.replaceArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeClient) ReplaceReturns(result1 kubernetes.Metadata, result2 error) { + fake.replaceMutex.Lock() + defer fake.replaceMutex.Unlock() + fake.ReplaceStub = nil + fake.replaceReturns = struct { + result1 kubernetes.Metadata + result2 error + }{result1, result2} +} + +func (fake *FakeClient) ReplaceReturnsOnCall(i int, result1 kubernetes.Metadata, result2 error) { + fake.replaceMutex.Lock() + defer fake.replaceMutex.Unlock() + fake.ReplaceStub = nil + if fake.replaceReturnsOnCall == nil { + fake.replaceReturnsOnCall = make(map[int]struct { + result1 kubernetes.Metadata + result2 error + }) + } + fake.replaceReturnsOnCall[i] = struct { + result1 kubernetes.Metadata + result2 error + }{result1, result2} +} + func (fake *FakeClient) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -1094,6 +1171,8 @@ func (fake *FakeClient) Invocations() map[string][][]interface{} { defer fake.patchMutex.RUnlock() fake.patchUsingStrategyMutex.RLock() defer fake.patchUsingStrategyMutex.RUnlock() + fake.replaceMutex.RLock() + defer fake.replaceMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value