Applyset dry run tests + ID value (#116265)

* Test for ApplySet with --dry-run=client|server

* Use the real format for ApplySet ID

* Incorporate feedback

* Adjustments from rebase

Kubernetes-commit: 6a31757f45693fec5ea4723bcb405ce4437e31ca
This commit is contained in:
Katrina Verey 2023-03-14 07:46:16 -04:00 committed by Kubernetes Publisher
parent c22c2cf969
commit ca98377c3e
7 changed files with 108 additions and 25 deletions

8
go.mod
View File

@ -30,10 +30,10 @@ require (
github.com/stretchr/testify v1.8.1
golang.org/x/sys v0.5.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.0.0-20230314010636-4c844db47d54
k8s.io/api v0.0.0-20230314091508-112a65bae227
k8s.io/apimachinery v0.0.0-20230314010357-128166500c57
k8s.io/cli-runtime v0.0.0-20230314020252-2497f10db39b
k8s.io/client-go v0.0.0-20230314011018-471f66fb1055
k8s.io/client-go v0.0.0-20230315061813-3cafc13f5d42
k8s.io/component-base v0.0.0-20230313212246-ce16dede9c0e
k8s.io/component-helpers v0.0.0-20230313212358-9baf6e0e627d
k8s.io/klog/v2 v2.90.1
@ -91,10 +91,10 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.0.0-20230314010636-4c844db47d54
k8s.io/api => k8s.io/api v0.0.0-20230314091508-112a65bae227
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230314010357-128166500c57
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20230314020252-2497f10db39b
k8s.io/client-go => k8s.io/client-go v0.0.0-20230314011018-471f66fb1055
k8s.io/client-go => k8s.io/client-go v0.0.0-20230315061813-3cafc13f5d42
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20230314010121-667a115fdda8
k8s.io/component-base => k8s.io/component-base v0.0.0-20230313212246-ce16dede9c0e
k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20230313212358-9baf6e0e627d

8
go.sum
View File

@ -531,14 +531,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.0.0-20230314010636-4c844db47d54 h1:0VSpq40qYJx9GN/G+BzJiVgcz3cR3xPc+xn3nQLAoZ8=
k8s.io/api v0.0.0-20230314010636-4c844db47d54/go.mod h1:YsFNxBfPYQZAIBg0XvL94rxDDVZvQQQUlo4KbwEEqNU=
k8s.io/api v0.0.0-20230314091508-112a65bae227 h1:Ak4YrHI4101ZMfx2hkXnp//d5r0nKXA8RkNaHCBJ7MA=
k8s.io/api v0.0.0-20230314091508-112a65bae227/go.mod h1:YsFNxBfPYQZAIBg0XvL94rxDDVZvQQQUlo4KbwEEqNU=
k8s.io/apimachinery v0.0.0-20230314010357-128166500c57 h1:Vr1geeI+at1NNCWyTN70NtPSNcveZ+fAcbZivzwHknM=
k8s.io/apimachinery v0.0.0-20230314010357-128166500c57/go.mod h1:1AlvkfXatlv5Kq9dCZg3Ksdu/DyrZ31Q0CncRqQ8Q9I=
k8s.io/cli-runtime v0.0.0-20230314020252-2497f10db39b h1:Q2m8dkuvdn9kQspNZGR10Yr8gjdQDOLdLgHLI3FcNG4=
k8s.io/cli-runtime v0.0.0-20230314020252-2497f10db39b/go.mod h1:0uP53DPxCWJvpFo91n4cTHSqkKGzx+PDunpv8Ic2EXg=
k8s.io/client-go v0.0.0-20230314011018-471f66fb1055 h1:J6JKsnjJepE9WwPlTuU5To88f7hkMzSHf72W8kjfEZY=
k8s.io/client-go v0.0.0-20230314011018-471f66fb1055/go.mod h1:2/gjZKF6uneNXNF3xsgmEUFbqNjlhWprJl4wv4CNd0c=
k8s.io/client-go v0.0.0-20230315061813-3cafc13f5d42 h1:b7qY4Bq8gHunC2mYT/RAtMWDgMmkMb16oQuktJkHpHI=
k8s.io/client-go v0.0.0-20230315061813-3cafc13f5d42/go.mod h1:oWqDJxiXYcSV7S8woQ4H5epopeK85SJyOu5lJsMndcU=
k8s.io/component-base v0.0.0-20230313212246-ce16dede9c0e h1:/mftAl/78q8dPZU8YkshNzt1XpbEJTGsxYZP/WGI4og=
k8s.io/component-base v0.0.0-20230313212246-ce16dede9c0e/go.mod h1:MQKfc/tXtS50042g1VxMb2W2E8PCt97xO8RsLTw3AeI=
k8s.io/component-helpers v0.0.0-20230313212358-9baf6e0e627d h1:c3f8fiRpmkt5PQw4lszj1F0CcMDOMqQsMy0zRInmw8o=

View File

@ -313,7 +313,6 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
if enforceNamespace && parent.IsNamespaced() {
parent.Namespace = namespace
}
// TODO: is version.Get() the right thing? Does it work for non-kubectl package consumers?
tooling := ApplySetTooling{name: baseName, version: ApplySetToolVersion}
restClient, err := f.ClientForMapping(parent.RESTMapping)
if err != nil || restClient == nil {
@ -419,8 +418,6 @@ func (o *ApplyOptions) Validate() error {
return fmt.Errorf("--selector is incompatible with --applyset")
} else if len(o.PruneResources) > 0 {
return fmt.Errorf("--prune-allowlist is incompatible with --applyset")
} else {
klog.Warning("WARNING: --prune --applyset is not fully implemented and does not yet prune any resources.")
}
} else {
if !o.All && o.Selector == "" {

View File

@ -2435,7 +2435,7 @@ metadata:
applyset.k8s.io/tooling: kubectl/v0.0.0-master+$Format:%H$
creationTimestamp: null
labels:
applyset.k8s.io/id: placeholder-todo
applyset.k8s.io/id: applyset-nqNkDlL072a9O3FBtGMDroXnF18TNtgUetAA6vsaglI-v1
name: mySet
namespace: test
`, string(createdSecret))
@ -2465,7 +2465,7 @@ metadata:
applyset.k8s.io/tooling: kubectl/v0.0.0-master+$Format:%H$
creationTimestamp: null
labels:
applyset.k8s.io/id: placeholder-todo
applyset.k8s.io/id: applyset-nqNkDlL072a9O3FBtGMDroXnF18TNtgUetAA6vsaglI-v1
name: mySet
namespace: test
`, string(updatedSecret))
@ -2496,7 +2496,7 @@ metadata:
applyset.k8s.io/tooling: kubectl/v0.0.0-master+$Format:%H$
creationTimestamp: null
labels:
applyset.k8s.io/id: placeholder-todo
applyset.k8s.io/id: applyset-nqNkDlL072a9O3FBtGMDroXnF18TNtgUetAA6vsaglI-v1
name: mySet
namespace: test
`, string(updatedSecret))
@ -2527,7 +2527,7 @@ metadata:
applyset.k8s.io/tooling: kubectl/v0.0.0-master+$Format:%H$
creationTimestamp: null
labels:
applyset.k8s.io/id: placeholder-todo
applyset.k8s.io/id: applyset-nqNkDlL072a9O3FBtGMDroXnF18TNtgUetAA6vsaglI-v1
name: mySet
namespace: test
`, string(updatedSecret))
@ -2577,7 +2577,7 @@ func TestApplySetInvalidLiveParent(t *testing.T) {
}),
}
}
validIDLabel := "placeholder-todo"
validIDLabel := "applyset-nqNkDlL072a9O3FBtGMDroXnF18TNtgUetAA6vsaglI-v1"
validToolingAnnotation := "kubectl/v1.27.0"
validGrsAnnotation := "deployments.apps,namespaces,secrets"
@ -2866,7 +2866,7 @@ metadata:
applyset.k8s.io/tooling: kubectl/v0.0.0-master+$Format:%H$
creationTimestamp: null
labels:
applyset.k8s.io/id: placeholder-todo
applyset.k8s.io/id: applyset-nqNkDlL072a9O3FBtGMDroXnF18TNtgUetAA6vsaglI-v1
name: mySet
namespace: test
`
@ -3147,3 +3147,75 @@ func fatalNoExit(t *testing.T, ioStreams genericclioptions.IOStreams) func(msg s
}
}
}
func TestApplySetDryRun(t *testing.T) {
cmdtesting.InitTestErrorHandler(t)
nameRC, rc := readReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
nameParentSecret := "mySet"
pathSecret := "/namespaces/test/secrets/" + nameParentSecret
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
// Scenario: the rc 'exists' server side but the applyset secret does not
// In dry run mode, non-dry run patch requests should not be made, and the secret should not be created
serverSideData := map[string][]byte{
pathRC: rc,
}
fakeDryRunClient := func(t *testing.T, allowPatch bool) *fake.RESTClient {
return &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "GET" {
data, ok := serverSideData[req.URL.Path]
if !ok {
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil
}
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil
}
if req.Method == "PATCH" && allowPatch && req.URL.Query().Get("dryRun") == "All" {
data, err := io.ReadAll(req.Body)
require.NoError(t, err)
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil
}
t.Fatalf("unexpected request: to %s\n%#v", req.URL.Path, req)
return nil, nil
}),
}
}
t.Run("server side dry run", func(t *testing.T) {
ioStreams, _, outbuff, _ := genericclioptions.NewTestIOStreams()
tf.Client = fakeDryRunClient(t, true)
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
cmd := NewCmdApply("kubectl", tf, ioStreams)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("server-side", "true")
cmd.Flags().Set("applyset", nameParentSecret)
cmd.Flags().Set("prune", "true")
cmd.Flags().Set("dry-run", "server")
cmd.Run(cmd, []string{})
})
assert.Equal(t, "replicationcontroller/test-rc serverside-applied (server dry run)\n", outbuff.String())
assert.Equal(t, len(serverSideData), 1, "unexpected creation")
require.Nil(t, serverSideData[pathSecret], "secret was created")
})
t.Run("client side dry run", func(t *testing.T) {
ioStreams, _, outbuff, _ := genericclioptions.NewTestIOStreams()
tf.Client = fakeDryRunClient(t, false)
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
cmd := NewCmdApply("kubectl", tf, ioStreams)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("applyset", nameParentSecret)
cmd.Flags().Set("prune", "true")
cmd.Flags().Set("dry-run", "client")
cmd.Run(cmd, []string{})
})
assert.Equal(t, "replicationcontroller/test-rc configured (dry run)\n", outbuff.String())
assert.Equal(t, len(serverSideData), 1, "unexpected creation")
require.Nil(t, serverSideData[pathSecret], "secret was created")
})
}

View File

@ -17,6 +17,8 @@ limitations under the License.
package apply
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"sort"
@ -59,10 +61,15 @@ const (
ApplySetGRsAnnotation = "applyset.k8s.io/contains-group-resources"
// ApplySetParentIDLabel is the key of the label that makes object an ApplySet parent object.
// Its value MUST be the base64 encoding of the hash of the GKNN of the object it is on,
// in the form base64(sha256(<name>.<namespace>.<kind>.<group>)), using the URL safe encoding of RFC4648.
// Its value MUST use the format specified in V1ApplySetIdFormat below
ApplySetParentIDLabel = "applyset.k8s.io/id"
// V1ApplySetIdFormat is the format required for the value of ApplySetParentIDLabel (and ApplysetPartOfLabel).
// The %s segment is the unique ID of the object itself, which MUST be the base64 encoding
// (using the URL safe encoding of RFC4648) of the hash of the GKNN of the object it is on, in the form:
// base64(sha256(<name>.<namespace>.<kind>.<group>)).
V1ApplySetIdFormat = "applyset-%s-v1"
// ApplysetPartOfLabel is the key of the label which indicates that the object is a member of an ApplySet.
// The value of the label MUST match the value of ApplySetParentIDLabel on the parent object.
ApplysetPartOfLabel = "applyset.k8s.io/part-of"
@ -141,10 +148,17 @@ func NewApplySet(parent *ApplySetParentRef, tooling ApplySetTooling, mapper meta
}
}
const applySetIDPartDelimiter = "."
// ID is the label value that we are using to identify this applyset.
// Format: base64(sha256(<name>.<namespace>.<kind>.<group>)), using the URL safe encoding of RFC4648.
func (a ApplySet) ID() string {
// TODO: base64(sha256(gknn))
return "placeholder-todo"
unencoded := strings.Join([]string{a.parentRef.Name, a.parentRef.Namespace, a.parentRef.GroupVersionKind.Kind, a.parentRef.GroupVersionKind.Group}, applySetIDPartDelimiter)
hashed := sha256.Sum256([]byte(unencoded))
b64 := base64.RawURLEncoding.EncodeToString(hashed[:])
// Label values must start and end with alphanumeric values, so add a known-safe prefix and suffix.
return fmt.Sprintf(V1ApplySetIdFormat, b64)
}
// Validate imposes restrictions on the parent object that is used to track the applyset.
@ -413,7 +427,7 @@ func generateResourcesAnnotation(resources sets.Set[schema.GroupVersionResource]
}
func (a ApplySet) FieldManager() string {
return fmt.Sprintf("%s-applyset-%s", a.toolingID.name, a.ID()) // TODO: validate this choice
return fmt.Sprintf("%s-applyset", a.toolingID.name)
}
// ParseApplySetParentRef creates a new ApplySetParentRef from a parent reference in the format [RESOURCE][.GROUP]/NAME

View File

@ -2,7 +2,7 @@ apiVersion: v1
kind: Namespace
metadata:
labels:
applyset.k8s.io/part-of: placeholder-todo
applyset.k8s.io/part-of: applyset-bjd1LnyQq0mtUu-riZCqjDQOmh0iNb9O2RcuT12WR0k-v1
name: foo
---
@ -11,5 +11,5 @@ apiVersion: v1
kind: Namespace
metadata:
labels:
applyset.k8s.io/part-of: placeholder-todo
applyset.k8s.io/part-of: applyset-bjd1LnyQq0mtUu-riZCqjDQOmh0iNb9O2RcuT12WR0k-v1
name: bar

View File

@ -2,5 +2,5 @@ apiVersion: v1
kind: Namespace
metadata:
labels:
applyset.k8s.io/part-of: placeholder-todo
applyset.k8s.io/part-of: applyset-bjd1LnyQq0mtUu-riZCqjDQOmh0iNb9O2RcuT12WR0k-v1
name: foo