Skip to content

Commit

Permalink
CN-3040 go-clouddriver add support for server side apply strategy (#181)
Browse files Browse the repository at this point in the history
* [CN-3040] replace deprecated wait.PollImmediate with wait.PollUntilContextTimeout

* [CN-3040] add server-side apply patch function

* [CN-3040] add force-conflicts check

* [CN-3040] remove encoding original object

* [CN-3040] add error logs

* [CN-3040] update Patch call

* [CN-3040] clean up errors
  • Loading branch information
pjberry16 authored Nov 26, 2024
1 parent 5e6e392 commit 26f65f2
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 23 deletions.
1 change: 1 addition & 0 deletions internal/kubernetes/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
AnnotationSpinnakerMonikerDetail = `moniker.spinnaker.io/detail`
AnnotationSpinnakerMonikerStack = `moniker.spinnaker.io/stack`
AnnotationSpinnakerStrategyVersioned = `strategy.spinnaker.io/versioned`
AnnotationSpinnakerServerSideApply = `strategy.spinnaker.io/server-side-apply`
)

// AddSpinnakerAnnotations adds Spinnaker-defined annotations to a given
Expand Down
52 changes: 34 additions & 18 deletions internal/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"fmt"

"github.com/homedepot/go-clouddriver/internal/kubernetes/patcher"
gcpatcher "github.com/homedepot/go-clouddriver/internal/kubernetes/patcher"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -58,6 +58,8 @@ type client struct {

// Apply a given manifest.
func (c *client) Apply(u *unstructured.Unstructured) (Metadata, error) {
var serverSideApply bool

metadata := Metadata{}
gvk := u.GroupVersionKind()

Expand Down Expand Up @@ -87,7 +89,7 @@ func (c *client) Apply(u *unstructured.Unstructured) (Metadata, error) {
ResourceVersion: restMapping.Resource.Version,
}

patcher, err := patcher.New(info, helper)
patcher, err := gcpatcher.New(info, helper)
if err != nil {
return metadata, err
}
Expand All @@ -100,27 +102,41 @@ func (c *client) Apply(u *unstructured.Unstructured) (Metadata, error) {
return metadata, err
}

if err := info.Get(); err != nil {
if !errors.IsNotFound(err) {
return metadata, err
}
// Check if server-side annotation is set.
if AnnotationMatches(*u, AnnotationSpinnakerServerSideApply, "true") {
serverSideApply = true
}

// Create the resource if it doesn't exist
// First, update the annotation used by kubectl apply
if err := util.CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil {
return metadata, err
}
// Server-side annotation can also be set to force-conflicts which will update your resources using server-side
// apply and becomes the sole manager.
if AnnotationMatches(*u, AnnotationSpinnakerServerSideApply, "force-conflicts") {
serverSideApply = true
patcher.Force = true
}

// Then create the resource and skip the three-way merge
obj, err := helper.Create(info.Namespace, true, info.Object)
if err != nil {
return metadata, err
}
if !serverSideApply {
if err := info.Get(); err != nil {
if !errors.IsNotFound(err) {
return metadata, err
}

_ = info.Refresh(obj, true)
// Create the resource if it doesn't exist
// First, update the annotation used by kubectl apply
if err := util.CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil {
return metadata, err
}

// Then create the resource and skip the three-way merge if not a server-side apply
obj, err := helper.Create(info.Namespace, true, info.Object)
if err != nil {
return metadata, err
}

_ = info.Refresh(obj, true)
}
}

_, patchedObject, err := patcher.Patch(info.Object, modified, info.Namespace, info.Name)
_, patchedObject, err := patcher.Patch(info.Object, modified, info.Namespace, info.Name, serverSideApply)
if err != nil {
return metadata, err
}
Expand Down
46 changes: 41 additions & 5 deletions internal/kubernetes/patcher/patcher.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package patcher

import (
"context"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -76,10 +77,13 @@ type Patcher struct {
// Patch tries to patch an OpenAPI resource. On success, returns the merge patch as well
// the final patched object. On failure, returns an error.
func (p *Patcher) Patch(current runtime.Object, modified []byte,
namespace, name string) ([]byte, runtime.Object, error) {
namespace, name string, serverSideApply bool) ([]byte, runtime.Object, error) {
var getErr error

patchBytes, patchObject, err := p.patchSimple(current, modified, namespace, name)
patchBytes, patchObject, err := p.patchSwitch(serverSideApply, current, modified, namespace, name)
if err != nil {
return nil, nil, err
}

if p.Retries == 0 {
p.Retries = maxPatchRetry
Expand All @@ -95,7 +99,7 @@ func (p *Patcher) Patch(current runtime.Object, modified []byte,
return nil, nil, getErr
}

patchBytes, patchObject, err = p.patchSimple(current, modified, namespace, name)
patchBytes, patchObject, err = p.patchSwitch(serverSideApply, current, modified, namespace, name)
}

if err != nil && (errors.IsConflict(err) || errors.IsInvalid(err)) && p.Force {
Expand Down Expand Up @@ -148,7 +152,7 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
case err != nil:
return nil, nil, err
case err == nil:
// Compute a three way strategic merge patch to send to server.
// Compute a three-way strategic merge patch to send to server.
patchType = types.StrategicMergePatchType

// Try to use openapi first if the openapi spec is available and can successfully calculate the patch.
Expand Down Expand Up @@ -194,12 +198,44 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
return patch, patchedObj, err
}

func (p *Patcher) patchServerSide(obj runtime.Object, namespace, name string) ([]byte, runtime.Object, error) {
patchType := types.ApplyPatchType

data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
if err != nil {
return nil, nil, err
}

options := metav1.PatchOptions{FieldManager: "spinnaker", Force: &p.Force}

if p.ResourceVersion != nil {
data, err = addResourceVersion(data, *p.ResourceVersion)
if err != nil {
return nil, nil, err
}
}

patchedObj, err := p.Helper.Patch(namespace, name, patchType, data, &options)

return data, patchedObj, err
}

// patchSwitch switches between a normal patch or a normal patch depending on whether the server-side annotation is set or not.
// This is set as a switch function so that during Patch if there are retries the correct function is called again.
func (p *Patcher) patchSwitch(serverSideApply bool, obj runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) {
if serverSideApply {
return p.patchServerSide(obj, namespace, name)
} else {
return p.patchSimple(obj, modified, namespace, name)
}
}

func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) {
if err := p.delete(namespace, name); err != nil {
return modified, nil, err
}
// TODO: use wait
if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) {
if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, p.Timeout, true, func(_ context.Context) (bool, error) {
if _, err := p.Helper.Get(namespace, name); !errors.IsNotFound(err) {
return false, err
}
Expand Down

0 comments on commit 26f65f2

Please sign in to comment.