Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/commands/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ func doApply(ctx context.Context, args []string, config applyCommandConfig) erro

waitPolicy := newWaitPolicy()
for _, ob := range objects {
opts.ApplyStrategy = applyStrategy(ob)
name := client.DisplayName(ob)
res, err := client.Sync(ctx, ob, opts)
if res != nil && res.GeneratedName != "" {
Expand Down Expand Up @@ -285,6 +286,7 @@ func newApplyCommand(cp ctxProvider) *cobra.Command {
c.Flags().BoolVar(&config.syncOptions.DisableCreate, "skip-create", false, "set to true to only update existing resources but not create new ones")
c.Flags().BoolVarP(&config.syncOptions.DryRun, "dry-run", "n", false, "dry-run, do not create/ update resources but show what would happen")
c.Flags().BoolVarP(&config.syncOptions.ShowSecrets, "show-secrets", "S", false, "do not obfuscate secret values in the output")
c.Flags().BoolVar(&config.syncOptions.ForceConflicts, "force-conflicts", false, "force field ownership conflicts when using server-side apply")
c.Flags().BoolVar(&config.showDetails, "show-details", false, "show details for object operations")
c.Flags().BoolVar(&config.gc, "gc", true, "garbage collect extra objects on the server")
c.Flags().BoolVar(&config.wait, "wait", false, "wait for changed objects to be ready")
Expand Down
19 changes: 19 additions & 0 deletions internal/commands/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ func TestApplyFlags(t *testing.T) {
s.assertErrorLineMatch(regexp.MustCompile(`\*\* dry-run mode, nothing was actually changed \*\*`))
}

func TestApplyServerSideApply(t *testing.T) {
s := newCustomScaffold(t, "testdata/projects/server-side-apply")
defer s.reset()
var captured []remote.SyncOptions
s.client.syncFunc = func(ctx context.Context, obj model.K8sLocalObject, opts remote.SyncOptions) (*remote.SyncResult, error) {
captured = append(captured, opts)
return &remote.SyncResult{Type: remote.SyncCreated}, nil
}
err := s.executeCommand("apply", "local", "--gc=false", "--force-conflicts")
require.NoError(t, err)
require.Len(t, captured, 2)
assert.ElementsMatch(t,
[]model.ApplyStrategy{model.ApplyStrategyServer, model.ApplyStrategyClient},
[]model.ApplyStrategy{captured[0].ApplyStrategy, captured[1].ApplyStrategy},
)
assert.True(t, captured[0].ForceConflicts)
assert.True(t, captured[1].ForceConflicts)
}

func TestApplyNamespaceClusterFilters(t *testing.T) {
tests := []struct {
name string
Expand Down
8 changes: 8 additions & 0 deletions internal/commands/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
const (
policyNever = "never"
policyDefault = "default"
policyServer = "server"
)

// isSet return true if the annotation name specified as directive is equal to the supplied value.
Expand Down Expand Up @@ -58,6 +59,13 @@ func isSet(ob model.K8sMeta, directive, value string, otherAllowedValues []strin

type updatePolicy struct{}

func applyStrategy(ob model.K8sMeta) model.ApplyStrategy {
if isSet(ob, model.QbecNames.Directives.ApplyStrategy, policyServer, []string{policyDefault}) {
return model.ApplyStrategyServer
}
return model.ApplyStrategyClient
}

func (u *updatePolicy) disableUpdate(ob model.K8sMeta) bool {
return isSet(ob, model.QbecNames.Directives.UpdatePolicy, policyNever, []string{policyDefault})
}
Expand Down
10 changes: 10 additions & 0 deletions internal/commands/directives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ func TestDirectivesUpdatePolicy(t *testing.T) {
a.True(ret)
}

func TestDirectivesApplyStrategy(t *testing.T) {
a := assert.New(t)
ret := applyStrategy(k8sMetaWithAnnotations("ConfigMap", "foo", "bar", nil))
a.Equal(model.ApplyStrategyClient, ret)
ret = applyStrategy(k8sMetaWithAnnotations("ConfigMap", "foo", "bar", map[string]interface{}{
"directives.qbec.io/apply-strategy": "server",
}))
a.Equal(model.ApplyStrategyServer, ret)
}

func TestDirectivesDeletePolicy(t *testing.T) {
dp := newDeletePolicy(func(gvk schema.GroupVersionKind) (bool, error) {
return gvk.Kind == "ConfigMap", nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ssa-config
annotations:
directives.qbec.io/apply-strategy: server
data:
foo: bar
---
apiVersion: v1
kind: ConfigMap
metadata:
name: client-config
data:
foo: baz
10 changes: 10 additions & 0 deletions internal/commands/testdata/projects/server-side-apply/qbec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
apiVersion: qbec.io/v1alpha1
kind: App
metadata:
name: server-side-apply
spec:
environments:
local:
context: kind-kind
defaultNamespace: default
18 changes: 10 additions & 8 deletions internal/model/external-names.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ const QBECDirectivesNamespace = "directives.qbec.io/"

// Directives is the list of directive names we support.
type Directives struct {
ApplyOrder string // numeric apply order for object
DeletePolicy string // delete policy "default" | "never"
UpdatePolicy string // update policy "default" | "never"
WaitPolicy string // wait policy "default" | "never"
ApplyOrder string // numeric apply order for object
ApplyStrategy string // apply strategy "default" | "server"
DeletePolicy string // delete policy "default" | "never"
UpdatePolicy string // update policy "default" | "never"
WaitPolicy string // wait policy "default" | "never"
}

// QbecNames is the set of names used by Qbec.
Expand Down Expand Up @@ -55,9 +56,10 @@ var QbecNames = struct {
DefaultNsVarName: QBECMetadataPrefix + "defaultNs",
CleanModeVarName: QBECMetadataPrefix + "cleanMode",
Directives: Directives{
ApplyOrder: QBECDirectivesNamespace + "apply-order",
DeletePolicy: QBECDirectivesNamespace + "delete-policy",
UpdatePolicy: QBECDirectivesNamespace + "update-policy",
WaitPolicy: QBECDirectivesNamespace + "wait-policy",
ApplyOrder: QBECDirectivesNamespace + "apply-order",
ApplyStrategy: QBECDirectivesNamespace + "apply-strategy",
DeletePolicy: QBECDirectivesNamespace + "delete-policy",
UpdatePolicy: QBECDirectivesNamespace + "update-policy",
WaitPolicy: QBECDirectivesNamespace + "wait-policy",
},
}
8 changes: 8 additions & 0 deletions internal/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@

//go:generate gen-qbec-swagger swagger.yaml swagger-schema.go

// ApplyStrategy controls how qbec updates objects on the cluster.
type ApplyStrategy string

const (
ApplyStrategyClient ApplyStrategy = "client"

Check failure on line 28 in internal/model/types.go

View workflow job for this annotation

GitHub Actions / Build

exported const ApplyStrategyClient should have comment (or a comment on this block) or be unexported
ApplyStrategyServer ApplyStrategy = "server"
)

// Environment points to a specific destination and has its own set of runtime parameters.
type Environment struct {
DefaultNamespace string `json:"defaultNamespace"` // default namespace to set for k8s context
Expand Down
Loading
Loading