diff --git a/Gopkg.lock b/Gopkg.lock index 7aac69fbf4..6a91c09b19 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -181,6 +181,12 @@ ] revision = "4d347d79dea0067c945f374f990601decb08abb5" +[[projects]] + branch = "master" + name = "github.com/mattbaird/jsonpatch" + packages = ["."] + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + [[projects]] branch = "master" name = "github.com/mitchellh/mapstructure" @@ -362,6 +368,7 @@ [[projects]] name = "k8s.io/api" packages = [ + "admission/v1beta1", "admissionregistration/v1alpha1", "admissionregistration/v1beta1", "apps/v1", @@ -616,6 +623,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "faa21da15f8a0ce6e5c6faafd268ac059c252eac32d222215645cdc308e61c70" + inputs-digest = "e4e6f9000b679617c61b4af906948d2587e5105ea38aac81dd788caf502ffd27" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index b25c511608..abbf4da474 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -42,4 +42,8 @@ [[constraint]] name = "google.golang.org/grpc" - version = "1.8.0" \ No newline at end of file + version = "1.8.0" + +[[constraint]] + branch = "master" + name = "github.com/mattbaird/jsonpatch" diff --git a/build/Makefile b/build/Makefile index 3357691970..32de45ec92 100644 --- a/build/Makefile +++ b/build/Makefile @@ -261,6 +261,7 @@ clean-gcloud-config: # (defaults virtualbox for Linux and macOS, hyperv for windows) if you so desire. minikube-test-cluster: minikube-agones-profile $(MINIKUBE) start --kubernetes-version v1.9.0 --vm-driver $(MINIKUBE_DRIVER) \ + --extra-config=apiserver.Admission.PluginNames=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota \ --extra-config=apiserver.Authorization.Mode=RBAC # wait until the master is up until docker run --rm $(common_mounts) --network=host -v $(minikube_cert_mount) $(DOCKER_RUN_ARGS) $(build_tag) kubectl cluster-info; \ diff --git a/build/install.yaml b/build/install.yaml index 0bc99ce6bc..4747e2158e 100644 --- a/build/install.yaml +++ b/build/install.yaml @@ -67,6 +67,42 @@ spec: initialDelaySeconds: 3 periodSeconds: 3 --- +apiVersion: v1 +kind: Service +metadata: + name: agones-controller-service + namespace: agones-system +spec: + selector: + stable.agones.dev/role: controller + ports: + - port: 443 + targetPort: 8081 +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: gameserver-mutation-webhook + namespace: agones-system +webhooks: + - name: gameserver-mutations.stable.agones.dev + failurePolicy: Fail + clientConfig: + service: + name: agones-controller-service + namespace: agones-system + path: /mutate-gameserver + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVLVENDQXhHZ0F3SUJBZ0lKQU9KUDY0MTB3dkdTTUEwR0NTcUdTSWIzRFFFQkN3VUFNSUdxTVFzd0NRWUQKVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFUE1BMEdBMVVFQ2d3R1FXZHZibVZ6TVE4dwpEUVlEVlFRTERBWkJaMjl1WlhNeE5EQXlCZ05WQkFNTUsyRm5iMjVsY3kxamIyNTBjbTlzYkdWeUxYTmxjblpwClkyVXVZV2R2Ym1WekxYTjVjM1JsYlM1emRtTXhMakFzQmdrcWhraUc5dzBCQ1FFV0gyRm5iMjVsY3kxa2FYTmoKZFhOelFHZHZiMmRzWldkeWIzVndjeTVqYjIwd0hoY05NVGd3TWpFME1EUTBORFEyV2hjTk1qZ3dNakV5TURRMApORFEyV2pDQnFqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeER6QU5CZ05WCkJBb01Ca0ZuYjI1bGN6RVBNQTBHQTFVRUN3d0dRV2R2Ym1Wek1UUXdNZ1lEVlFRRERDdGhaMjl1WlhNdFkyOXUKZEhKdmJHeGxjaTF6WlhKMmFXTmxMbUZuYjI1bGN5MXplWE4wWlcwdWMzWmpNUzR3TEFZSktvWklodmNOQVFrQgpGaDloWjI5dVpYTXRaR2x6WTNWemMwQm5iMjluYkdWbmNtOTFjSE11WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCCkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXpnVlQ5MGVqeE5ud0NvL09qTUQyNmZVNGRya1NlZndkUWd3aWJpZmEKbDhyazZZMFZ2T0lWMUgrbFJvd2UwNm1XTnVSNUZPWEZBMGZYbHZ4Q0tLWVZRcFNQRUsyWVN5aC9hU25KUUw2cQpvOGVBWVRKQmtPWUxCNUNiekl6aVdlb1FmT1lOOE1sRW44YlhKZGllSmhISDhVbnlqdHlvVGx4emhabVgrcGZ0CmhVZGVhM1Zrek8yMW40K1FFM1JYNWYxMzJGVEZjdXFYT1VBL3BpOGNjQU5HYzN6akxlWkp2QTlvZFBFaEdmN2cKQzhleUE2OFNWY3NoK1BqejBsdzk1QVB2bE12MWptcVVSRldjRVNUTGFRMEZ4NUt3UnlWMHppWm1VdkFBRjJaeApEWmhIVWNvRlBIQXdUbDc1TkFobkhwTWxMTnA1TDd0Y1ZkeVQ4QjJHUnMrc2xRSURBUUFCbzFBd1RqQWRCZ05WCkhRNEVGZ1FVZ3YxblRQYVFKU04zTHFtNWpJalc0eEhtZEcwd0h3WURWUjBqQkJnd0ZvQVVndjFuVFBhUUpTTjMKTHFtNWpJalc0eEhtZEcwd0RBWURWUjBUQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBSEtFQwprdEVqWU5VQ0ErbXlzejRvclc3cFJVdmhCSERWU2dzWTZlRVZSTHpmLzF5SVpFMHU2NTZrcEs2T1Q3TWhKR2xVCkt3R1NTb1VCQnpWZ1VzWmpEbTdQZ2JrNGlZem40TTF4THpiTFFCcjNNYzV6WEhlZlB2YmltaEQ1NWNMenBWRnUKVlFtQm1aVjJOalU1RHVTZFJuZGxjUGFOY2cvdU9jdlpLNEtZMUtDQkEzRW9BUUlrcHpIWDJpVU1veGlSdlpWTgpORXdnRlR0SUdCWW4wSGZML3ZnT3NIOGZWck1Va3VHMnZoR2RlWEJwWmlxL0JaSmJaZU4yckNmMmdhWDFRSXYwCkVLYmN1RnFNOThXVDVaVlpSdFgxWTNSd2V2ZzRteFlKWEN1SDZGRjlXOS9TejI5NEZ5Mk9CS0I4SkFWYUV4OW4KMS9pNmZJZmZHbkhUWFdIc1ZRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + rules: + - apiGroups: + - stable.agones.dev + resources: + - "gameservers" + apiVersions: + - "v1alpha1" + operations: + - CREATE +--- # Service account, secret, role and rolebinding for sidecar (agones-sdk) pod apiVersion: v1 kind: ServiceAccount @@ -159,4 +195,4 @@ subjects: roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: agones-controller + name: agones-controller \ No newline at end of file diff --git a/cmd/controller/Dockerfile b/cmd/controller/Dockerfile index d61054c88f..77a72285a0 100644 --- a/cmd/controller/Dockerfile +++ b/cmd/controller/Dockerfile @@ -1,6 +1,7 @@ FROM alpine:3.7 COPY ./bin/controller /home/agones/controller +COPY ./certs /home/agones/certs RUN apk --update add ca-certificates && \ adduser -D agones && \ chown -R agones /home/agones && \ diff --git a/cmd/controller/certs/cert.sh b/cmd/controller/certs/cert.sh new file mode 100755 index 0000000000..4520c16724 --- /dev/null +++ b/cmd/controller/certs/cert.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +echo "Generating certs..." +echo "Email should be: emailAddress=agones-discuss@googlegroups.com" +echo "Common Name should be: agones-controller-service.agones-system.svc" +openssl genrsa -out server.key 2048 +openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 + +echo "caBundle:" +base64 -w 0 server.crt + +echo "done" \ No newline at end of file diff --git a/cmd/controller/certs/server.crt b/cmd/controller/certs/server.crt new file mode 100644 index 0000000000..9ae350ca21 --- /dev/null +++ b/cmd/controller/certs/server.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKTCCAxGgAwIBAgIJAOJP6410wvGSMA0GCSqGSIb3DQEBCwUAMIGqMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKU29tZS1TdGF0ZTEPMA0GA1UECgwGQWdvbmVzMQ8w +DQYDVQQLDAZBZ29uZXMxNDAyBgNVBAMMK2Fnb25lcy1jb250cm9sbGVyLXNlcnZp +Y2UuYWdvbmVzLXN5c3RlbS5zdmMxLjAsBgkqhkiG9w0BCQEWH2Fnb25lcy1kaXNj +dXNzQGdvb2dsZWdyb3Vwcy5jb20wHhcNMTgwMjE0MDQ0NDQ2WhcNMjgwMjEyMDQ0 +NDQ2WjCBqjELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxDzANBgNV +BAoMBkFnb25lczEPMA0GA1UECwwGQWdvbmVzMTQwMgYDVQQDDCthZ29uZXMtY29u +dHJvbGxlci1zZXJ2aWNlLmFnb25lcy1zeXN0ZW0uc3ZjMS4wLAYJKoZIhvcNAQkB +Fh9hZ29uZXMtZGlzY3Vzc0Bnb29nbGVncm91cHMuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAzgVT90ejxNnwCo/OjMD26fU4drkSefwdQgwibifa +l8rk6Y0VvOIV1H+lRowe06mWNuR5FOXFA0fXlvxCKKYVQpSPEK2YSyh/aSnJQL6q +o8eAYTJBkOYLB5CbzIziWeoQfOYN8MlEn8bXJdieJhHH8UnyjtyoTlxzhZmX+pft +hUdea3VkzO21n4+QE3RX5f132FTFcuqXOUA/pi8ccANGc3zjLeZJvA9odPEhGf7g +C8eyA68SVcsh+Pjz0lw95APvlMv1jmqURFWcESTLaQ0Fx5KwRyV0ziZmUvAAF2Zx +DZhHUcoFPHAwTl75NAhnHpMlLNp5L7tcVdyT8B2GRs+slQIDAQABo1AwTjAdBgNV +HQ4EFgQUgv1nTPaQJSN3Lqm5jIjW4xHmdG0wHwYDVR0jBBgwFoAUgv1nTPaQJSN3 +Lqm5jIjW4xHmdG0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHKEC +ktEjYNUCA+mysz4orW7pRUvhBHDVSgsY6eEVRLzf/1yIZE0u656kpK6OT7MhJGlU +KwGSSoUBBzVgUsZjDm7Pgbk4iYzn4M1xLzbLQBr3Mc5zXHefPvbimhD55cLzpVFu +VQmBmZV2NjU5DuSdRndlcPaNcg/uOcvZK4KY1KCBA3EoAQIkpzHX2iUMoxiRvZVN +NEwgFTtIGBYn0HfL/vgOsH8fVrMUkuG2vhGdeXBpZiq/BZJbZeN2rCf2gaX1QIv0 +EKbcuFqM98WT5ZVZRtX1Y3Rwevg4mxYJXCuH6FF9W9/Sz294Fy2OBKB8JAVaEx9n +1/i6fIffGnHTXWHsVQ== +-----END CERTIFICATE----- diff --git a/cmd/controller/certs/server.key b/cmd/controller/certs/server.key new file mode 100644 index 0000000000..99a7687c9d --- /dev/null +++ b/cmd/controller/certs/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzgVT90ejxNnwCo/OjMD26fU4drkSefwdQgwibifal8rk6Y0V +vOIV1H+lRowe06mWNuR5FOXFA0fXlvxCKKYVQpSPEK2YSyh/aSnJQL6qo8eAYTJB +kOYLB5CbzIziWeoQfOYN8MlEn8bXJdieJhHH8UnyjtyoTlxzhZmX+pfthUdea3Vk +zO21n4+QE3RX5f132FTFcuqXOUA/pi8ccANGc3zjLeZJvA9odPEhGf7gC8eyA68S +Vcsh+Pjz0lw95APvlMv1jmqURFWcESTLaQ0Fx5KwRyV0ziZmUvAAF2ZxDZhHUcoF +PHAwTl75NAhnHpMlLNp5L7tcVdyT8B2GRs+slQIDAQABAoIBAEoU5GKQ4jTQ4V4K +5Az8/kyWnx0h46D1pVewoVjW/+WBUdshnmVzLsJgu/+oNxWJb7iBY4C+Np+9X6qt +PuT7A74TSXaH1bGA+H/KRNIBPb7y6BkLR0RhVCn+N+fP6TzHy/H9j5m75e9GQusa +/5NU5X7ARnZUpji3SdsKpfm4U/KOV+p2jWSPX7HOBu/KYa1jveCt6JMPQ36KBXkR +MlVCkADcvAAGuObpa/sm0MA63+ihdeSYhkEXKqxH4Az3PVDLwP80T8f5VqwWYmnq +L/Bg6HnV5GHnlTXA+WepNbHkokN9G0H6m0Itj4al3bGTB4jeBCJZp5FHW5obJ4qP +WkcfXcECgYEA8UIZvdNSsSpRZVTe4hhtXncOptFTdoVKIzTuUVKfKe9OB5i32B9u +4hDNpBDqkgEktg4R8/cn57z6ZUdUCcobjWQbJLXE5wp7Htf7jCGnL2QmIcp20rbF +HrE2lHr0oNr3MLUruBvArVB3LLGmOEgn3NQw9aJwg/542EDS7x5v3XkCgYEA2pwG +HiGYXTJfYNg/+SmDuSDWFe4iMPTRw1TT85uGE9UErYPQd8OqYQQOgIAT7xp1z0oq +6pmsjPO6HZng9oHKeAO/JPeSuXE+bJ8HYKp7W929F+LGJoqitOLPMm/9OovYDCvh +6+qDY5LNRHdAeTqwwI2yugf4YnBjY6zIfBp5LP0CgYEAyrrv5JqSbzuPQGZMEJPU +O8Ax+K4Hw52HygPtizqxcsybtjh3rE3loGPcWdS5OE1rquwx299BkjMz+i0xCjTi +aDLJuFRiDH+7LBT0VTHmSiWPAXAf3zskc4EYyzZzIEQ/2Zc0ELaJd1oZet4hPkQr +8x3/sjl48QHCTH5UggkCmYkCgYAfK/pPV5kDSQiCpbNRkxLeVglQ7Tjg5Df482KZ +rQaMU2asW0xhl3v3A34R4rF0+b/sw/WkqC8LlkFmsSd73vwA6v/ZhJfea4BsOqzx +or2eVtr8yfBZVJFo26KR3ZgtPf2blrJLUpBTpX4xkhOWdcD4Y/wlPLe1SbNSZjPc +RmYa/QKBgQC6a7zNASP0A0qMPhqyJZlbg4F8WMRtgHFfsy/iLwdiEUGuChQWULkc +hzPWpjD0zwrSxZtyhSL0b6THnqTiin23OvtINlViZmos5mWW0VeGsbHnJuRa3THy +EjSkSP4mR7tJlJokboZ+lYL7gABHn2ERnDsqX8oEU6QpDP1rZhYSzg== +-----END RSA PRIVATE KEY----- diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 05cc86725a..04969b626c 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -18,6 +18,8 @@ package main import ( "strings" "time" + "os" + "path/filepath" "agones.dev/agones/pkg" "agones.dev/agones/pkg/client/clientset/versioned" @@ -25,6 +27,7 @@ import ( "agones.dev/agones/pkg/gameservers" "agones.dev/agones/pkg/util/runtime" "agones.dev/agones/pkg/util/signals" + "agones.dev/agones/pkg/webhooks" "github.com/sirupsen/logrus" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -39,6 +42,8 @@ const ( pullSidecarFlag = "always-pull-sidecar" minPortFlag = "min-port" maxPortFlag = "max-port" + certFileFlag = "cert-file" + keyFileFlag = "key-file" ) func init() { @@ -47,13 +52,23 @@ func init() { // main starts the operator for the gameserver CRD func main() { + exec, err := os.Executable() + if err != nil { + logrus.WithError(err).Fatal("Could not get executable path") + } + + base := filepath.Dir(exec) viper.SetDefault(sidecarFlag, "gcr.io/agones-images/agones-sdk:"+pkg.Version) viper.SetDefault(pullSidecarFlag, false) + viper.SetDefault(certFileFlag, filepath.Join(base, "certs/server.crt")) + viper.SetDefault(keyFileFlag, filepath.Join(base, "certs/server.key")) pflag.String(sidecarFlag, viper.GetString(sidecarFlag), "Flag to overwrite the GameServer sidecar image that is used. Can also use SIDECAR env variable") pflag.Bool(pullSidecarFlag, viper.GetBool(pullSidecarFlag), "For development purposes, set the sidecar image to have a ImagePullPolicy of Always. Can also use ALWAYS_PULL_SIDECAR env variable") pflag.Int32(minPortFlag, 0, "Required. The minimum port that that a GameServer can be allocated to. Can also use MIN_PORT env variable.") pflag.Int32(maxPortFlag, 0, "Required. The maximum port that that a GameServer can be allocated to. Can also use MAX_PORT env variable") + pflag.String(keyFileFlag, viper.GetString(certFileFlag), "Optional. Path to the key file") + pflag.String(certFileFlag, viper.GetString(certFileFlag), "Optional. Path to the crt file") pflag.Parse() viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) @@ -61,16 +76,22 @@ func main() { runtime.Must(viper.BindEnv(pullSidecarFlag)) runtime.Must(viper.BindEnv(minPortFlag)) runtime.Must(viper.BindEnv(maxPortFlag)) + runtime.Must(viper.BindEnv(keyFileFlag)) + runtime.Must(viper.BindEnv(certFileFlag)) runtime.Must(viper.BindPFlags(pflag.CommandLine)) minPort := int32(viper.GetInt64(minPortFlag)) maxPort := int32(viper.GetInt64(maxPortFlag)) sidecarImage := viper.GetString(sidecarFlag) alwaysPullSidecar := viper.GetBool(pullSidecarFlag) + keyFile := viper.GetString(keyFileFlag) + certFile := viper.GetString(certFileFlag) logrus.WithField(sidecarFlag, sidecarImage). WithField("minPort", minPort). WithField("maxPort", maxPort). + WithField(keyFileFlag, keyFile). + WithField(certFileFlag, certFile). WithField("alwaysPullSidecarImage", alwaysPullSidecar). WithField("Version", pkg.Version).Info("starting gameServer operator...") @@ -102,13 +123,21 @@ func main() { agonesInformerFactory := externalversions.NewSharedInformerFactory(agonesClient, 30*time.Second) kubeInformationFactory := informers.NewSharedInformerFactory(kubeClient, 30*time.Second) - c := gameservers.NewController(minPort, maxPort, sidecarImage, alwaysPullSidecar, kubeClient, kubeInformationFactory, extClient, agonesClient, agonesInformerFactory) + + wh := webhooks.NewWebHook(certFile, keyFile) + c := gameservers.NewController(wh, minPort, maxPort, sidecarImage, alwaysPullSidecar, kubeClient, kubeInformationFactory, extClient, agonesClient, agonesInformerFactory) stop := signals.NewStopChannel() kubeInformationFactory.Start(stop) agonesInformerFactory.Start(stop) + go func() { + if err := wh.Run(stop); err != nil { // nolint: vetshadow + logrus.WithError(err).Fatal("could not run webhook server") + } + }() + err = c.Run(2, stop) if err != nil { logrus.WithError(err).Fatal("Could not run gameserver controller") diff --git a/install.yaml b/install.yaml index 970c771574..16a8887985 100644 --- a/install.yaml +++ b/install.yaml @@ -66,6 +66,42 @@ spec: initialDelaySeconds: 3 periodSeconds: 3 --- +apiVersion: v1 +kind: Service +metadata: + name: agones-controller-service + namespace: agones-system +spec: + selector: + stable.agones.dev/role: controller + ports: + - port: 443 + targetPort: 8081 +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: gameserver-mutation-webhook + namespace: agones-system +webhooks: + - name: gameserver-mutations.stable.agones.dev + failurePolicy: Fail + clientConfig: + service: + name: agones-controller-service + namespace: agones-system + path: /mutate-gameserver + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVLVENDQXhHZ0F3SUJBZ0lKQU9KUDY0MTB3dkdTTUEwR0NTcUdTSWIzRFFFQkN3VUFNSUdxTVFzd0NRWUQKVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLVTI5dFpTMVRkR0YwWlRFUE1BMEdBMVVFQ2d3R1FXZHZibVZ6TVE4dwpEUVlEVlFRTERBWkJaMjl1WlhNeE5EQXlCZ05WQkFNTUsyRm5iMjVsY3kxamIyNTBjbTlzYkdWeUxYTmxjblpwClkyVXVZV2R2Ym1WekxYTjVjM1JsYlM1emRtTXhMakFzQmdrcWhraUc5dzBCQ1FFV0gyRm5iMjVsY3kxa2FYTmoKZFhOelFHZHZiMmRzWldkeWIzVndjeTVqYjIwd0hoY05NVGd3TWpFME1EUTBORFEyV2hjTk1qZ3dNakV5TURRMApORFEyV2pDQnFqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeER6QU5CZ05WCkJBb01Ca0ZuYjI1bGN6RVBNQTBHQTFVRUN3d0dRV2R2Ym1Wek1UUXdNZ1lEVlFRRERDdGhaMjl1WlhNdFkyOXUKZEhKdmJHeGxjaTF6WlhKMmFXTmxMbUZuYjI1bGN5MXplWE4wWlcwdWMzWmpNUzR3TEFZSktvWklodmNOQVFrQgpGaDloWjI5dVpYTXRaR2x6WTNWemMwQm5iMjluYkdWbmNtOTFjSE11WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCCkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXpnVlQ5MGVqeE5ud0NvL09qTUQyNmZVNGRya1NlZndkUWd3aWJpZmEKbDhyazZZMFZ2T0lWMUgrbFJvd2UwNm1XTnVSNUZPWEZBMGZYbHZ4Q0tLWVZRcFNQRUsyWVN5aC9hU25KUUw2cQpvOGVBWVRKQmtPWUxCNUNiekl6aVdlb1FmT1lOOE1sRW44YlhKZGllSmhISDhVbnlqdHlvVGx4emhabVgrcGZ0CmhVZGVhM1Zrek8yMW40K1FFM1JYNWYxMzJGVEZjdXFYT1VBL3BpOGNjQU5HYzN6akxlWkp2QTlvZFBFaEdmN2cKQzhleUE2OFNWY3NoK1BqejBsdzk1QVB2bE12MWptcVVSRldjRVNUTGFRMEZ4NUt3UnlWMHppWm1VdkFBRjJaeApEWmhIVWNvRlBIQXdUbDc1TkFobkhwTWxMTnA1TDd0Y1ZkeVQ4QjJHUnMrc2xRSURBUUFCbzFBd1RqQWRCZ05WCkhRNEVGZ1FVZ3YxblRQYVFKU04zTHFtNWpJalc0eEhtZEcwd0h3WURWUjBqQkJnd0ZvQVVndjFuVFBhUUpTTjMKTHFtNWpJalc0eEhtZEcwd0RBWURWUjBUQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBSEtFQwprdEVqWU5VQ0ErbXlzejRvclc3cFJVdmhCSERWU2dzWTZlRVZSTHpmLzF5SVpFMHU2NTZrcEs2T1Q3TWhKR2xVCkt3R1NTb1VCQnpWZ1VzWmpEbTdQZ2JrNGlZem40TTF4THpiTFFCcjNNYzV6WEhlZlB2YmltaEQ1NWNMenBWRnUKVlFtQm1aVjJOalU1RHVTZFJuZGxjUGFOY2cvdU9jdlpLNEtZMUtDQkEzRW9BUUlrcHpIWDJpVU1veGlSdlpWTgpORXdnRlR0SUdCWW4wSGZML3ZnT3NIOGZWck1Va3VHMnZoR2RlWEJwWmlxL0JaSmJaZU4yckNmMmdhWDFRSXYwCkVLYmN1RnFNOThXVDVaVlpSdFgxWTNSd2V2ZzRteFlKWEN1SDZGRjlXOS9TejI5NEZ5Mk9CS0I4SkFWYUV4OW4KMS9pNmZJZmZHbkhUWFdIc1ZRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + rules: + - apiGroups: + - stable.agones.dev + resources: + - "gameservers" + apiVersions: + - "v1alpha1" + operations: + - CREATE +--- # Service account, secret, role and rolebinding for sidecar (agones-sdk) pod apiVersion: v1 kind: ServiceAccount diff --git a/pkg/apis/stable/v1alpha1/types.go b/pkg/apis/stable/v1alpha1/types.go index bb186ef4e7..784d7d3d2f 100644 --- a/pkg/apis/stable/v1alpha1/types.go +++ b/pkg/apis/stable/v1alpha1/types.go @@ -23,8 +23,10 @@ import ( ) const ( - // Creating is when the Pod for the GameServer is being created, - // but they have yet to register themselves yet as Ready + // PortAllocation is for when a dynamically allocating GameServer + // is being created, an open port needs to be allocated + PortAllocation State = "PortAllocation" + // Creating is before the Pod for the GameServer is being created Creating State = "Creating" // Starting is for when the Pods for the GameServer are being // created but have yet to register themselves as Ready @@ -161,7 +163,11 @@ func (gs *GameServer) ApplyDefaults() { } if gs.Status.State == "" { - gs.Status.State = Creating + if gs.Spec.PortPolicy == Dynamic { + gs.Status.State = PortAllocation + } else { + gs.Status.State = Creating + } } // health @@ -178,6 +184,25 @@ func (gs *GameServer) ApplyDefaults() { } } +// Validate validates the GameServer configuration. +// If a GameServer is invalid there will be > 0 values in +// the returned array +func (gs *GameServer) Validate() []metav1.StatusCause { + var causes []metav1.StatusCause + + // make sure the container value points to a valid container + _, _, err := gs.FindGameServerContainer() + if err != nil { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "container", + Message: err.Error(), + }) + } + + return causes +} + // FindGameServerContainer returns the container that is specified in // spec.gameServer.container. Returns the index and the value. // Returns an error if not found diff --git a/pkg/apis/stable/v1alpha1/types_test.go b/pkg/apis/stable/v1alpha1/types_test.go index 05f3c99008..32b86f3900 100644 --- a/pkg/apis/stable/v1alpha1/types_test.go +++ b/pkg/apis/stable/v1alpha1/types_test.go @@ -73,7 +73,7 @@ func TestGameServerApplyDefaults(t *testing.T) { container: "testing", expected: expected{ protocol: "UDP", - state: Creating, + state: PortAllocation, policy: Dynamic, health: Health{ Disabled: false, @@ -115,6 +115,26 @@ func TestGameServerApplyDefaults(t *testing.T) { }, }, }, + "set basic defaults on static gameserver": { + gameServer: GameServer{ + Spec: GameServerSpec{ + PortPolicy: Static, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}}}, + }, + container: "testing", + expected: expected{ + protocol: "UDP", + state: Creating, + policy: Static, + health: Health{ + Disabled: false, + FailureThreshold: 3, + InitialDelaySeconds: 5, + PeriodSeconds: 5, + }, + }, + }, "health is disabled": { gameServer: GameServer{ Spec: GameServerSpec{ @@ -125,7 +145,7 @@ func TestGameServerApplyDefaults(t *testing.T) { container: "testing", expected: expected{ protocol: "UDP", - state: Creating, + state: PortAllocation, policy: Dynamic, health: Health{ Disabled: true, @@ -148,6 +168,27 @@ func TestGameServerApplyDefaults(t *testing.T) { } } +func TestGameServerValidate(t *testing.T) { + gs := GameServer{ + Spec: GameServerSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}}}, + } + gs.ApplyDefaults() + assert.Empty(t, gs.Validate()) + + gs = GameServer{ + Spec: GameServerSpec{ + Container: "nope", + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}}}, + } + causes := gs.Validate() + assert.Len(t, causes, 1) + assert.Equal(t, causes[0].Field, "container") + assert.Equal(t, causes[0].Type, metav1.CauseTypeFieldValueInvalid) +} + func TestGameServerPod(t *testing.T) { fixture := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"}, Spec: GameServerSpec{ diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go index 98c8e4be71..72861f00d7 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -15,6 +15,7 @@ package gameservers import ( + "encoding/json" "fmt" "net/http" "time" @@ -26,8 +27,11 @@ import ( "agones.dev/agones/pkg/client/informers/externalversions" listerv1alpha1 "agones.dev/agones/pkg/client/listers/stable/v1alpha1" "agones.dev/agones/pkg/util/runtime" + "agones.dev/agones/pkg/webhooks" + "github.com/mattbaird/jsonpatch" "github.com/pkg/errors" "github.com/sirupsen/logrus" + admv1beta1 "k8s.io/api/admission/v1beta1" corev1 "k8s.io/api/core/v1" apiv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" @@ -72,7 +76,9 @@ type Controller struct { } // NewController returns a new gameserver crd controller -func NewController(minPort, maxPort int32, +func NewController( + wh *webhooks.WebHook, + minPort, maxPort int32, sidecarImage string, alwaysPullSidecarImage bool, kubeClient kubernetes.Interface, @@ -105,6 +111,8 @@ func NewController(minPort, maxPort int32, recorder: recorder, } + wh.AddHandler("/mutate-gameserver", c.mutationHandler) + gsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.enqueueGameServer, UpdateFunc: func(oldObj, newObj interface{}) { @@ -148,6 +156,73 @@ func NewController(minPort, maxPort int32, return c } +// mutationHandler is the handler for the mutating webhook that sets the +// the default values on the GameServer, and validates the results +func (c *Controller) mutationHandler(review admv1beta1.AdmissionReview) (admv1beta1.AdmissionReview, error) { + logrus.WithField("review", review).Info("mutationHandler") + + if !(review.Request.Operation == admv1beta1.Create && + review.Request.Kind.Group == stable.GroupName && + review.Request.Kind.Kind == "GameServer") { + + logrus.WithField("review", review).Warn("Skipping mutationHandler, because invalid operation") + return review, nil + } + + obj := review.Request.Object + gs := &stablev1alpha1.GameServer{} + err := json.Unmarshal(obj.Raw, gs) + if err != nil { + return review, errors.Wrapf(err, "error unmarshalling original GameServer json: %s", obj.Raw) + } + + // This is the main logic of this function + // the rest is really just json plumbing + gs.ApplyDefaults() + causes := gs.Validate() + if len(causes) > 0 { + review.Response.Allowed = false + details := metav1.StatusDetails{ + Name: review.Request.Name, + Group: review.Request.Kind.Group, + Kind: review.Request.Kind.Kind, + Causes: causes, + } + review.Response.Result = &metav1.Status{ + Status: metav1.StatusFailure, + Message: "GameServer configuration is invalid: " + details.String(), + Reason: metav1.StatusReasonInvalid, + Details: &details, + } + + logrus.WithField("review", review).Info("Invalid GameServer") + return review, nil + } + + new, err := json.Marshal(gs) + if err != nil { + return review, errors.Wrapf(err, "error marshalling default applied GameSever %s to json", gs.ObjectMeta.Name) + } + + patch, err := jsonpatch.CreatePatch(obj.Raw, new) + if err != nil { + return review, errors.Wrapf(err, "error creating patch for GameServer %s", gs.ObjectMeta.Name) + } + + json, err := json.Marshal(patch) + if err != nil { + return review, errors.Wrapf(err, "error creating json for patch for GameServer %s", gs.ObjectMeta.Name) + } + + logrus.WithField("gs", gs.ObjectMeta.Name).WithField("patch", string(json)).Infof("patch created!") + + pt := admv1beta1.PatchTypeJSONPatch + review.Response.PatchType = &pt + review.Response.Patch = json + + return review, nil +} + // Run the GameServer controller. Will block until stop is closed. // Runs threadiness number workers to process the rate limited queue func (c Controller) Run(threadiness int, stop <-chan struct{}) error { @@ -269,7 +344,7 @@ func (c *Controller) syncGameServer(key string) error { if gs, err = c.syncGameServerDeletionTimestamp(gs); err != nil { return err } - if gs, err = c.syncGameServerBlankState(gs); err != nil { + if gs, err = c.syncGameServerPortAllocationState(gs); err != nil { return err } if gs, err = c.syncGameServerCreatingState(gs); err != nil { @@ -326,33 +401,31 @@ func (c *Controller) syncGameServerDeletionTimestamp(gs *stablev1alpha1.GameServ return gs, errors.Wrapf(err, "error removing finalizer for GameServer %s", gsCopy.ObjectMeta.Name) } -// syncGameServerBlankState applies default values to the the GameServer if its state is "" (blank) -// returns an updated GameServer -func (c *Controller) syncGameServerBlankState(gs *stablev1alpha1.GameServer) (*stablev1alpha1.GameServer, error) { - if !(gs.Status.State == "" && gs.ObjectMeta.DeletionTimestamp.IsZero()) { +// syncGameServerPortAllocationState gives a port to a dynamically allocating GameServer +func (c *Controller) syncGameServerPortAllocationState(gs *stablev1alpha1.GameServer) (*stablev1alpha1.GameServer, error) { + if !(gs.Status.State == stablev1alpha1.PortAllocation && gs.ObjectMeta.DeletionTimestamp.IsZero()) { return gs, nil } gsCopy := gs.DeepCopy() - gsCopy.ApplyDefaults() - // manage dynamic ports - if gsCopy.Spec.PortPolicy == stablev1alpha1.Dynamic { - port, err := c.portAllocator.Allocate() - if err != nil { - return gsCopy, errors.Wrapf(err, "error allocating port for GameServer %s", gsCopy.Name) - } - gsCopy.Spec.HostPort = port + port, err := c.portAllocator.Allocate() + if err != nil { + return gsCopy, errors.Wrapf(err, "error allocating port for GameServer %s", gsCopy.Name) } + gsCopy.Spec.HostPort = port + gsCopy.Status.State = stablev1alpha1.Creating + c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Port allocated") - logrus.WithField("gs", gsCopy).Info("Syncing Blank State") - var err error + logrus.WithField("gs", gsCopy).Info("Syncing Port Allocation State") gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(gsCopy) if err != nil { + // if the GameServer doesn't get updated with the port data, then put the port + // back in the pool, as it will get retried on the next pass + c.portAllocator.DeAllocate(port) return gs, errors.Wrapf(err, "error updating GameServer %s to default values", gs.Name) } - c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Defaults applied") return gs, nil } diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go index 1c4f01a546..68ea03176e 100644 --- a/pkg/gameservers/controller_test.go +++ b/pkg/gameservers/controller_test.go @@ -20,11 +20,16 @@ import ( "sync" "testing" "time" + "encoding/json" "agones.dev/agones/pkg/apis/stable" "agones.dev/agones/pkg/apis/stable/v1alpha1" + "agones.dev/agones/pkg/webhooks" + "github.com/json-iterator/go" + "github.com/mattbaird/jsonpatch" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + admv1beta1 "k8s.io/api/admission/v1beta1" corev1 "k8s.io/api/core/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -86,9 +91,21 @@ func TestSyncGameServer(t *testing.T) { c, mocks := newFakeController() updateCount := 0 podCreated := false - fixture := v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, - Spec: newSingleContainerSpec()} + fixture := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: v1alpha1.GameServerSpec{ + ContainerPort: 7777, + Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, + }, + }, + }, + } + + fixture.ApplyDefaults() + mocks.kubeClient.AddReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &corev1.NodeList{Items: []corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "node1"}}}}, nil + }) mocks.kubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { ca := action.(k8stesting.CreateAction) pod := ca.GetObject().(*corev1.Pod) @@ -97,7 +114,7 @@ func TestSyncGameServer(t *testing.T) { return false, pod, nil }) mocks.agonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { - gameServers := &v1alpha1.GameServerList{Items: []v1alpha1.GameServer{fixture}} + gameServers := &v1alpha1.GameServerList{Items: []v1alpha1.GameServer{*fixture}} return true, gameServers, nil }) mocks.agonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { @@ -117,10 +134,12 @@ func TestSyncGameServer(t *testing.T) { return true, gs, nil }) - _, cancel := startInformers(mocks, c.gameServerSynced) + stop, cancel := startInformers(mocks, c.gameServerSynced) defer cancel() + err := c.portAllocator.Run(stop) + assert.Nil(t, err) - err := c.syncHandler("default/test") + err = c.syncHandler("default/test") assert.Nil(t, err) assert.Equal(t, 2, updateCount, "update reactor should twice") assert.True(t, podCreated, "pod should be created") @@ -229,6 +248,137 @@ func TestHealthCheck(t *testing.T) { testHTTPHealth(t, "http://localhost:8080/healthz", "ok", http.StatusOK) } +func TestMutationHandler(t *testing.T) { + t.Parallel() + + c, _ := newFakeController() + gvk := metav1.GroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind("GameServer")) + + t.Run("not create", func(t *testing.T) { + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: gvk, + Operation: admv1beta1.Update, + }, + } + + result, err := c.mutationHandler(review) + assert.Nil(t, err) + assert.Equal(t, review, result) + }) + + t.Run("not agones group", func(t *testing.T) { + nongroup := gvk.DeepCopy() + nongroup.Group = "broken" + + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: *nongroup, + Operation: admv1beta1.Create, + }, + } + + result, err := c.mutationHandler(review) + assert.Nil(t, err) + assert.Equal(t, review, result) + }) + + t.Run("not gameserver kind", func(t *testing.T) { + nonkind := gvk.DeepCopy() + nonkind.Kind = "broken" + + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: *nonkind, + Operation: admv1beta1.Create, + }, + } + + result, err := c.mutationHandler(review) + assert.Nil(t, err) + assert.Equal(t, review, result) + }) + + t.Run("gameserver defaults", func(t *testing.T) { + fixture := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: newSingleContainerSpec()} + + raw, err := jsoniter.Marshal(fixture) + assert.Nil(t, err) + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: gvk, + Operation: admv1beta1.Create, + Object: runtime.RawExtension{ + Raw: raw, + }, + }, + Response: &admv1beta1.AdmissionResponse{Allowed: true}, + } + + result, err := c.mutationHandler(review) + assert.Nil(t, err) + assert.True(t, result.Response.Allowed) + assert.Equal(t, admv1beta1.PatchTypeJSONPatch, *result.Response.PatchType) + + patch := &jsonpatch.ByPath{} + err = json.Unmarshal(result.Response.Patch, patch) + assert.Nil(t, err) + + assertContains := func(patch *jsonpatch.ByPath, op jsonpatch.JsonPatchOperation) { + found := false + for _, p := range *patch { + if assert.ObjectsAreEqualValues(p, op) { + found = true + } + } + + assert.True(t, found, "Could not find operation %#v in patch %v", op, *patch) + } + + assertContains(patch, jsonpatch.JsonPatchOperation{Operation: "add", Path: "/metadata/finalizers", Value: []interface{}{"stable.agones.dev"}}) + assertContains(patch, jsonpatch.JsonPatchOperation{Operation: "add", Path: "/spec/protocol", Value: "UDP"}) + }) + + t.Run("invalid gameserver", func(t *testing.T) { + fixture := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: v1alpha1.GameServerSpec{ + Container: "NOPE!", + ContainerPort: 7777, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "container", Image: "container/image"}, + {Name: "container2", Image: "container/image"}, + }, + }, + }, + }, + } + raw, err := jsoniter.Marshal(fixture) + assert.Nil(t, err) + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: gvk, + Operation: admv1beta1.Create, + Object: runtime.RawExtension{ + Raw: raw, + }, + }, + Response: &admv1beta1.AdmissionResponse{Allowed: true}, + } + + result, err := c.mutationHandler(review) + assert.Nil(t, err) + assert.False(t, result.Response.Allowed) + assert.Equal(t, metav1.StatusFailure, review.Response.Result.Status) + assert.Equal(t, metav1.StatusReasonInvalid, review.Response.Result.Reason) + assert.Equal(t, review.Request.Kind.Kind, result.Response.Result.Details.Kind) + assert.Equal(t, review.Request.Kind.Group, result.Response.Result.Details.Group) + assert.NotEmpty(t, result.Response.Result.Details.Causes) + }) +} + func TestSyncGameServerDeletionTimestamp(t *testing.T) { t.Parallel() @@ -293,33 +443,10 @@ func TestSyncGameServerDeletionTimestamp(t *testing.T) { }) } -func TestSyncGameServerBlankState(t *testing.T) { +func TestSyncGameServerPortAllocationState(t *testing.T) { t.Parallel() - t.Run("GameServer with a blank initial state", func(t *testing.T) { - c, mocks := newFakeController() - fixture := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, Spec: newSingleContainerSpec()} - updated := false - - mocks.agonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { - updated = true - ua := action.(k8stesting.UpdateAction) - gs := ua.GetObject().(*v1alpha1.GameServer) - assert.Equal(t, fixture.ObjectMeta.Name, gs.ObjectMeta.Name) - assert.Equal(t, fixture.ObjectMeta.Namespace, gs.ObjectMeta.Namespace) - return true, gs, nil - }) - - result, err := c.syncGameServerBlankState(fixture) - assert.Nil(t, err, "sync should not error") - assert.True(t, updated, "update should occur") - assert.Equal(t, fixture.ObjectMeta.Name, result.ObjectMeta.Name) - assert.Equal(t, fixture.ObjectMeta.Namespace, result.ObjectMeta.Namespace) - assert.Equal(t, v1alpha1.Creating, result.Status.State) - assert.Equal(t, fmt.Sprintf("%s %s %s", corev1.EventTypeNormal, v1alpha1.Creating, "Defaults applied"), <-mocks.fakeRecorder.Events) - }) - - t.Run("Gameserver with dynamic port state", func(t *testing.T) { + t.Run("Gameserver with port allocation state", func(t *testing.T) { t.Parallel() c, mocks := newFakeController() fixture := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, @@ -331,7 +458,9 @@ func TestSyncGameServerBlankState(t *testing.T) { }, }, }, + Status: v1alpha1.GameServerStatus{State: v1alpha1.PortAllocation}, } + fixture.ApplyDefaults() mocks.kubeClient.AddReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) { return true, &corev1.NodeList{Items: []corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "node1"}}}}, nil }) @@ -355,7 +484,7 @@ func TestSyncGameServerBlankState(t *testing.T) { err := c.portAllocator.Run(stop) assert.Nil(t, err) - result, err := c.syncGameServerBlankState(fixture) + result, err := c.syncGameServerPortAllocationState(fixture) assert.Nil(t, err, "sync should not error") assert.True(t, updated, "update should occur") assert.Equal(t, v1alpha1.Dynamic, result.Spec.PortPolicy) @@ -365,13 +494,13 @@ func TestSyncGameServerBlankState(t *testing.T) { t.Run("Gameserver with unknown state", func(t *testing.T) { testNoChange(t, "Unknown", func(c *Controller, fixture *v1alpha1.GameServer) (*v1alpha1.GameServer, error) { - return c.syncGameServerBlankState(fixture) + return c.syncGameServerPortAllocationState(fixture) }) }) t.Run("GameServer with non zero deletion datetime", func(t *testing.T) { testWithNonZeroDeletionTimestamp(t, v1alpha1.Shutdown, func(c *Controller, fixture *v1alpha1.GameServer) (*v1alpha1.GameServer, error) { - return c.syncGameServerBlankState(fixture) + return c.syncGameServerPortAllocationState(fixture) }) }) } @@ -778,7 +907,8 @@ func testWithNonZeroDeletionTimestamp(t *testing.T, state v1alpha1.State, f func // newFakeController returns a controller, backed by the fake Clientset func newFakeController() (*Controller, mocks) { m := newMocks() - c := NewController(10, 20, "sidecar:dev", false, + wh := webhooks.NewWebHook("", "") + c := NewController(wh, 10, 20, "sidecar:dev", false, m.kubeClient, m.kubeInformationFactory, m.extClient, m.agonesClient, m.agonesInformerFactory) c.recorder = m.fakeRecorder return c, m diff --git a/pkg/gameservers/portallocator.go b/pkg/gameservers/portallocator.go index 86dc261c13..4c79b3a812 100644 --- a/pkg/gameservers/portallocator.go +++ b/pkg/gameservers/portallocator.go @@ -136,6 +136,13 @@ func (pa *PortAllocator) Allocate() (int32, error) { return -1, ErrPortNotFound } +// DeAllocate marks the given port as no longer allocated +func (pa *PortAllocator) DeAllocate(port int32) { + pa.mutex.Lock() + defer pa.mutex.Unlock() + pa.portAllocations = setPortAllocation(port, pa.portAllocations, false) +} + // syncAddNode adds another node port section // to the available ports func (pa *PortAllocator) syncAddNode(obj interface{}) { @@ -156,12 +163,9 @@ func (pa *PortAllocator) syncAddNode(obj interface{}) { // syncDeleteGameServer when a GameServer Pod is deleted // make the HostPort available func (pa *PortAllocator) syncDeleteGameServer(object interface{}) { - pa.mutex.Lock() - defer pa.mutex.Unlock() - gs := object.(*v1alpha1.GameServer) logrus.WithField("gs", gs).Info("syncing deleted GameServer") - pa.portAllocations = setPortAllocation(gs.Spec.HostPort, pa.portAllocations, false) + pa.DeAllocate(gs.Spec.HostPort) } // syncPortAllocations syncs the pod, node and gameserver caches then diff --git a/pkg/gameservers/portallocator_test.go b/pkg/gameservers/portallocator_test.go index 86119e051b..cb1bb9031d 100644 --- a/pkg/gameservers/portallocator_test.go +++ b/pkg/gameservers/portallocator_test.go @@ -123,6 +123,30 @@ func TestPortAllocatorMultithreadAllocate(t *testing.T) { wg.Wait() } +func TestPortAllocatorDeAllocate(t *testing.T) { + t.Parallel() + + m := newMocks() + pa := NewPortAllocator(10, 20, m.kubeInformationFactory, m.agonesInformerFactory) + nodes := []corev1.Node{n1, n2, n3} + m.kubeClient.AddReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) { + nl := &corev1.NodeList{Items: nodes} + return true, nl, nil + }) + stop, cancel := startInformers(m) + defer cancel() + err := pa.Run(stop) + assert.Nil(t, err) + + port, err := pa.Allocate() + assert.Nil(t, err) + assert.True(t, port >= 10) + assert.Equal(t, 1, countAllocatedPorts(pa, port)) + + pa.DeAllocate(port) + assert.Equal(t, 0, countAllocatedPorts(pa, port)) +} + func TestPortAllocatorSyncPortAllocations(t *testing.T) { t.Parallel() diff --git a/pkg/webhooks/webhooks.go b/pkg/webhooks/webhooks.go new file mode 100644 index 0000000000..30b22525c3 --- /dev/null +++ b/pkg/webhooks/webhooks.go @@ -0,0 +1,121 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package webhooks manages and receives Kubernetes Webhooks +package webhooks + +import ( + "encoding/json" + "net/http" + + "agones.dev/agones/pkg/util/runtime" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/api/admission/v1beta1" +) + +// Handler handles a webhook's AdmissionReview coming in, and will return the +// AdmissionReview that will be the return value of the webhook +type Handler func(review v1beta1.AdmissionReview) (v1beta1.AdmissionReview, error) + +// Server is a http server interface to enable easier testing +type Server interface { + Close() error + ListenAndServeTLS(certFile, keyFile string) error +} + +// WebHook manage Kubernetes webhooks +type WebHook struct { + mux *http.ServeMux + server Server + certFile string + keyFile string + handlers map[string][]Handler +} + +// NewWebHook returns a Kubernetes webhook manager +func NewWebHook(certFile, keyFile string) *WebHook { + mux := http.NewServeMux() + server := http.Server{ + Addr: ":8081", + Handler: mux, + } + + return &WebHook{ + mux: mux, + server: &server, + certFile: certFile, + keyFile: keyFile, + handlers: map[string][]Handler{}, + } +} + +// Run runs the webhook server, starting a https listener. +// Will block on stop channel +func (wh *WebHook) Run(stop <-chan struct{}) error { + go func() { + <-stop + wh.server.Close() // nolint: errcheck + }() + + logrus.WithField("webook", wh).Infof("webhook: https server started") + + err := wh.server.ListenAndServeTLS(wh.certFile, wh.keyFile) + if err == http.ErrServerClosed { + logrus.WithError(err).Info("webhook: https server closed") + } + + return errors.Wrap(err, "Could not listen on :8081") +} + +// AddHandler adds a handler for a given path +func (wh *WebHook) AddHandler(path string, h Handler) { + if len(wh.handlers[path]) == 0 { + wh.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + err := wh.handle(path, w, r) + if err != nil { + runtime.HandleError(logrus.WithField("url", r.URL), err) + w.WriteHeader(http.StatusInternalServerError) + } + }) + } + wh.handlers[path] = append(wh.handlers[path], h) +} + +// handle Handles http requests for webhooks +func (wh *WebHook) handle(path string, w http.ResponseWriter, r *http.Request) error { + logrus.WithField("path", path).Info("running webhook") + var review v1beta1.AdmissionReview + err := json.NewDecoder(r.Body).Decode(&review) + if err != nil { + return errors.Wrapf(err, "error decoding decoding json for path %v", path) + } + + // set it to true, in case there are no handlers + if review.Response == nil { + review.Response = &v1beta1.AdmissionResponse{Allowed: true} + } + for _, h := range wh.handlers[path] { + review, err = h(review) + if err != nil { + return errors.Wrapf(err, "error with webhook handler for path %v", path) + } + } + err = json.NewEncoder(w).Encode(review) + if err != nil { + return errors.Wrapf(err, "error decoding encoding json for path %v", path) + } + + return nil +} diff --git a/pkg/webhooks/webhooks_test.go b/pkg/webhooks/webhooks_test.go new file mode 100644 index 0000000000..81636f3a9e --- /dev/null +++ b/pkg/webhooks/webhooks_test.go @@ -0,0 +1,98 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package webhooks + +import ( + "net/http" + "net/http/httptest" + "testing" + + "bytes" + "encoding/json" + + "github.com/stretchr/testify/assert" + "k8s.io/api/admission/v1beta1" +) + +type testServer struct { + server *httptest.Server +} + +func (ts *testServer) Close() error { + ts.server.Close() + return nil +} + +// ListenAndServeTLS(certFile, keyFile string) error +func (ts *testServer) ListenAndServeTLS(certFile, keyFile string) error { + ts.server.StartTLS() + return nil +} + +func TestWebHookAddHandler(t *testing.T) { + t.Parallel() + + fixtures := map[string]struct { + handlerCount int + }{ + "one": {handlerCount: 1}, + "two": {handlerCount: 2}, + } + + for k, handles := range fixtures { + t.Run(k, func(t *testing.T) { + + stop := make(chan struct{}) + defer close(stop) + + fixture := v1beta1.AdmissionReview{Request: &v1beta1.AdmissionRequest{UID: "1234"}} + callCount := 0 + + wh := NewWebHook("", "") + ts := &testServer{server: httptest.NewUnstartedServer(wh.mux)} + wh.server = ts + + for i := 0; i < handles.handlerCount; i++ { + wh.AddHandler("/test", func(review v1beta1.AdmissionReview) (v1beta1.AdmissionReview, error) { + assert.Equal(t, fixture.Request, review.Request) + assert.True(t, review.Response.Allowed) + callCount++ + + return review, nil + }) + } + + err := wh.Run(stop) + assert.Nil(t, err) + + client := ts.server.Client() + url := ts.server.URL + "/test" + + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(fixture) + assert.Nil(t, err) + + r, err := http.NewRequest("GET", url, buf) + assert.Nil(t, err) + + resp, err := client.Do(r) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + assert.Equal(t, handles.handlerCount, callCount, "[%v] /test should have been called", k) + }) + } + +} diff --git a/vendor/github.com/mattbaird/jsonpatch/.gitignore b/vendor/github.com/mattbaird/jsonpatch/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/mattbaird/jsonpatch/LICENSE b/vendor/github.com/mattbaird/jsonpatch/LICENSE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/mattbaird/jsonpatch/README.md b/vendor/github.com/mattbaird/jsonpatch/README.md new file mode 100644 index 0000000000..91c03b3fc5 --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/README.md @@ -0,0 +1,46 @@ +# jsonpatch +As per http://jsonpatch.com/ JSON Patch is specified in RFC 6902 from the IETF. + +JSON Patch allows you to generate JSON that describes changes you want to make to a document, so you don't have to send the whole doc. JSON Patch format is supported by HTTP PATCH method, allowing for standards based partial updates via REST APIs. + +```bash +go get github.com/mattbaird/jsonpatch +``` + +I tried some of the other "jsonpatch" go implementations, but none of them could diff two json documents and +generate format like jsonpatch.com specifies. Here's an example of the patch format: + +```json +[ + { "op": "replace", "path": "/baz", "value": "boo" }, + { "op": "add", "path": "/hello", "value": ["world"] }, + { "op": "remove", "path": "/foo"} +] + +``` +The API is super simple +#example +```go +package main + +import ( + "fmt" + "github.com/mattbaird/jsonpatch" +) + +var simpleA = `{"a":100, "b":200, "c":"hello"}` +var simpleB = `{"a":100, "b":200, "c":"goodbye"}` + +func main() { + patch, e := jsonpatch.CreatePatch([]byte(simpleA), []byte(simpleA)) + if e != nil { + fmt.Printf("Error creating JSON patch:%v", e) + return + } + for _, operation := range patch { + fmt.Printf("%s\n", operation.Json()) + } +} +``` + +This code needs more tests, as it's a highly recursive, type-fiddly monster. It's not a lot of code, but it has to deal with a lot of complexity. diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go new file mode 100644 index 0000000000..295f260f5a --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go @@ -0,0 +1,257 @@ +package jsonpatch + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") + +type JsonPatchOperation struct { + Operation string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func (j *JsonPatchOperation) Json() string { + b, _ := json.Marshal(j) + return string(b) +} + +func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) { + var b bytes.Buffer + b.WriteString("{") + b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation)) + b.WriteString(fmt.Sprintf(`,"path":"%s"`, j.Path)) + // Consider omitting Value for non-nullable operations. + if j.Value != nil || j.Operation == "replace" || j.Operation == "add" { + v, err := json.Marshal(j.Value) + if err != nil { + return nil, err + } + b.WriteString(`,"value":`) + b.Write(v) + } + b.WriteString("}") + return b.Bytes(), nil +} + +type ByPath []JsonPatchOperation + +func (a ByPath) Len() int { return len(a) } +func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path } + +func NewPatch(operation, path string, value interface{}) JsonPatchOperation { + return JsonPatchOperation{Operation: operation, Path: path, Value: value} +} + +// CreatePatch creates a patch as specified in http://jsonpatch.com/ +// +// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content. +// The function will return an array of JsonPatchOperations +// +// An error will be returned if any of the two documents are invalid. +func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) { + aI := map[string]interface{}{} + bI := map[string]interface{}{} + err := json.Unmarshal(a, &aI) + if err != nil { + return nil, errBadJSONDoc + } + err = json.Unmarshal(b, &bI) + if err != nil { + return nil, errBadJSONDoc + } + return diff(aI, bI, "", []JsonPatchOperation{}) +} + +// Returns true if the values matches (must be json types) +// The types of the values must match, otherwise it will always return false +// If two map[string]interface{} are given, all elements must match. +func matchesValue(av, bv interface{}) bool { + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + return false + } + switch at := av.(type) { + case string: + bt := bv.(string) + if bt == at { + return true + } + case float64: + bt := bv.(float64) + if bt == at { + return true + } + case bool: + bt := bv.(bool) + if bt == at { + return true + } + case map[string]interface{}: + bt := bv.(map[string]interface{}) + for key := range at { + if !matchesValue(at[key], bt[key]) { + return false + } + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + case []interface{}: + bt := bv.([]interface{}) + if len(bt) != len(at) { + return false + } + for key := range at { + if !matchesValue(at[key], bt[key]) { + return false + } + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + } + return false +} + +// From http://tools.ietf.org/html/rfc6901#section-4 : +// +// Evaluation of each reference token begins by decoding any escaped +// character sequence. This is performed by first transforming any +// occurrence of the sequence '~1' to '/', and then transforming any +// occurrence of the sequence '~0' to '~'. +// TODO decode support: +// var rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") + +var rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1") + +func makePath(path string, newPart interface{}) string { + key := rfc6901Encoder.Replace(fmt.Sprintf("%v", newPart)) + if path == "" { + return "/" + key + } + if strings.HasSuffix(path, "/") { + return path + key + } + return path + "/" + key +} + +// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. +func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { + for key, bv := range b { + p := makePath(path, key) + av, ok := a[key] + // value was added + if !ok { + patch = append(patch, NewPatch("add", p, bv)) + continue + } + // If types have changed, replace completely + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + patch = append(patch, NewPatch("replace", p, bv)) + continue + } + // Types are the same, compare values + var err error + patch, err = handleValues(av, bv, p, patch) + if err != nil { + return nil, err + } + } + // Now add all deleted values as nil + for key := range a { + _, found := b[key] + if !found { + p := makePath(path, key) + + patch = append(patch, NewPatch("remove", p, nil)) + } + } + return patch, nil +} + +func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { + var err error + switch at := av.(type) { + case map[string]interface{}: + bt := bv.(map[string]interface{}) + patch, err = diff(at, bt, p, patch) + if err != nil { + return nil, err + } + case string, float64, bool: + if !matchesValue(av, bv) { + patch = append(patch, NewPatch("replace", p, bv)) + } + case []interface{}: + bt, ok := bv.([]interface{}) + if !ok { + // array replaced by non-array + patch = append(patch, NewPatch("replace", p, bv)) + } else if len(at) != len(bt) { + // arrays are not the same length + patch = append(patch, compareArray(at, bt, p)...) + + } else { + for i := range bt { + patch, err = handleValues(at[i], bt[i], makePath(p, i), patch) + if err != nil { + return nil, err + } + } + } + case nil: + switch bv.(type) { + case nil: + // Both nil, fine. + default: + patch = append(patch, NewPatch("add", p, bv)) + } + default: + panic(fmt.Sprintf("Unknown type:%T ", av)) + } + return patch, nil +} + +func compareArray(av, bv []interface{}, p string) []JsonPatchOperation { + retval := []JsonPatchOperation{} + // var err error + for i, v := range av { + found := false + for _, v2 := range bv { + if reflect.DeepEqual(v, v2) { + found = true + break + } + } + if !found { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) + } + } + + for i, v := range bv { + found := false + for _, v2 := range av { + if reflect.DeepEqual(v, v2) { + found = true + break + } + } + if !found { + retval = append(retval, NewPatch("add", makePath(p, i), v)) + } + } + + return retval +} diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch_complex_test.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_complex_test.go new file mode 100644 index 0000000000..2a98871ffe --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_complex_test.go @@ -0,0 +1,88 @@ +package jsonpatch + +import ( + "github.com/stretchr/testify/assert" + "sort" + "testing" +) + +var complexBase = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}}` +var complexA = `{"a":100, "b":[{"c1":"goodbye", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}}` +var complexB = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":100, "g":"h", "i":"j"}}` +var complexC = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}, "k":[{"l":"m"}, {"l":"o"}]}` +var complexD = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"}, {"c3":"hello3", "d3":"foo3"} ], "e":{"f":200, "g":"h", "i":"j"}}` +var complexE = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}}` + +func TestComplexSame(t *testing.T) { + patch, e := CreatePatch([]byte(complexBase), []byte(complexBase)) + assert.NoError(t, e) + assert.Equal(t, 0, len(patch), "they should be equal") +} +func TestComplexOneStringReplaceInArray(t *testing.T) { + patch, e := CreatePatch([]byte(complexBase), []byte(complexA)) + assert.NoError(t, e) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/b/0/c1", change.Path, "they should be equal") + assert.Equal(t, "goodbye", change.Value, "they should be equal") +} + +func TestComplexOneIntReplace(t *testing.T) { + patch, e := CreatePatch([]byte(complexBase), []byte(complexB)) + assert.NoError(t, e) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/e/f", change.Path, "they should be equal") + var expected float64 = 100 + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestComplexOneAdd(t *testing.T) { + patch, e := CreatePatch([]byte(complexBase), []byte(complexC)) + assert.NoError(t, e) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/k", change.Path, "they should be equal") + a := make(map[string]interface{}) + b := make(map[string]interface{}) + a["l"] = "m" + b["l"] = "o" + expected := []interface{}{a, b} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestComplexOneAddToArray(t *testing.T) { + patch, e := CreatePatch([]byte(complexBase), []byte(complexC)) + assert.NoError(t, e) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/k", change.Path, "they should be equal") + a := make(map[string]interface{}) + b := make(map[string]interface{}) + a["l"] = "m" + b["l"] = "o" + expected := []interface{}{a, b} + assert.Equal(t, expected, change.Value, "they should be equal") +} + +func TestComplexVsEmpty(t *testing.T) { + patch, e := CreatePatch([]byte(complexBase), []byte(empty)) + assert.NoError(t, e) + assert.Equal(t, 3, len(patch), "they should be equal") + sort.Sort(ByPath(patch)) + change := patch[0] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/a", change.Path, "they should be equal") + + change = patch[1] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/b", change.Path, "they should be equal") + + change = patch[2] + assert.Equal(t, "remove", change.Operation, "they should be equal") + assert.Equal(t, "/e", change.Path, "they should be equal") +} diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch_geojson_test.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_geojson_test.go new file mode 100644 index 0000000000..6a92da0389 --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_geojson_test.go @@ -0,0 +1,48 @@ +package jsonpatch + +import ( + "github.com/stretchr/testify/assert" + "sort" + "testing" +) + +var point = `{"type":"Point", "coordinates":[0.0, 1.0]}` +var lineString = `{"type":"LineString", "coordinates":[[0.0, 1.0], [2.0, 3.0]]}` + +func TestPointLineStringReplace(t *testing.T) { + patch, e := CreatePatch([]byte(point), []byte(lineString)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 3, "they should be equal") + sort.Sort(ByPath(patch)) + change := patch[0] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/coordinates/0", "they should be equal") + assert.Equal(t, change.Value, []interface{}{0.0, 1.0}, "they should be equal") + change = patch[1] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/coordinates/1", "they should be equal") + assert.Equal(t, change.Value, []interface{}{2.0, 3.0}, "they should be equal") + change = patch[2] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/type", "they should be equal") + assert.Equal(t, change.Value, "LineString", "they should be equal") +} + +func TestLineStringPointReplace(t *testing.T) { + patch, e := CreatePatch([]byte(lineString), []byte(point)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 3, "they should be equal") + sort.Sort(ByPath(patch)) + change := patch[0] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/coordinates/0", "they should be equal") + assert.Equal(t, change.Value, 0.0, "they should be equal") + change = patch[1] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/coordinates/1", "they should be equal") + assert.Equal(t, change.Value, 1.0, "they should be equal") + change = patch[2] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/type", "they should be equal") + assert.Equal(t, change.Value, "Point", "they should be equal") +} diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch_hypercomplex_test.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_hypercomplex_test.go new file mode 100644 index 0000000000..f34423b4bc --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_hypercomplex_test.go @@ -0,0 +1,181 @@ +package jsonpatch + +import ( + "github.com/stretchr/testify/assert" + "sort" + "testing" +) + +var hyperComplexBase = ` +{ + "goods": [ + { + "id": "0001", + "type": "donut", + "name": "Cake", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" }, + { "id": "1003", "type": "Blueberry" }, + { "id": "1004", "type": "Devil's Food" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5007", "type": "Powdered Sugar" }, + { "id": "5006", "type": "Chocolate with Sprinkles" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + }, + { + "id": "0002", + "type": "donut", + "name": "Raised", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + }, + { + "id": "0003", + "type": "donut", + "name": "Old Fashioned", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + } +] +}` + +var hyperComplexA = ` +{ + "goods": [ + { + "id": "0001", + "type": "donut", + "name": "Cake", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" }, + { "id": "1003", "type": "Strawberry" }, + { "id": "1004", "type": "Devil's Food" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5007", "type": "Powdered Sugar" }, + { "id": "5006", "type": "Chocolate with Sprinkles" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + }, + { + "id": "0002", + "type": "donut", + "name": "Raised", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5005", "type": "Sugar" }, + { "id": "5003", "type": "Chocolate" }, + { "id": "5004", "type": "Maple" } + ] + }, + { + "id": "0003", + "type": "donut", + "name": "Old Fashioned", + "ppu": 0.55, + "batters": + { + "batter": + [ + { "id": "1001", "type": "Regular" }, + { "id": "1002", "type": "Chocolate" }, + { "id": "1003", "type": "Vanilla" } + ] + }, + "topping": + [ + { "id": "5001", "type": "None" }, + { "id": "5002", "type": "Glazed" }, + { "id": "5004", "type": "Maple" } + ] + } +] +}` + +func TestHyperComplexSame(t *testing.T) { + patch, e := CreatePatch([]byte(hyperComplexBase), []byte(hyperComplexBase)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 0, "they should be equal") +} + +func TestHyperComplexBoolReplace(t *testing.T) { + patch, e := CreatePatch([]byte(hyperComplexBase), []byte(hyperComplexA)) + assert.NoError(t, e) + assert.Equal(t, 3, len(patch), "they should be equal") + sort.Sort(ByPath(patch)) + + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/goods/0/batters/batter/2/type", change.Path, "they should be equal") + assert.Equal(t, "Strawberry", change.Value, "they should be equal") + change = patch[1] + assert.Equal(t, "add", change.Operation, "they should be equal") + assert.Equal(t, "/goods/2/batters/batter/2", change.Path, "they should be equal") + assert.Equal(t, map[string]interface{}{"id": "1003", "type": "Vanilla"}, change.Value, "they should be equal") + change = patch[2] + assert.Equal(t, change.Operation, "remove", "they should be equal") + assert.Equal(t, change.Path, "/goods/2/topping/2", "they should be equal") + assert.Equal(t, nil, change.Value, "they should be equal") +} diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch_json_test.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_json_test.go new file mode 100644 index 0000000000..4f8617f75b --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_json_test.go @@ -0,0 +1,31 @@ +package jsonpatch + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMarshalNullableValue(t *testing.T) { + p1 := JsonPatchOperation{ + Operation: "replace", + Path: "/a1", + Value: nil, + } + assert.JSONEq(t, `{"op":"replace", "path":"/a1","value":null}`, p1.Json()) + + p2 := JsonPatchOperation{ + Operation: "replace", + Path: "/a2", + Value: "v2", + } + assert.JSONEq(t, `{"op":"replace", "path":"/a2", "value":"v2"}`, p2.Json()) +} + +func TestMarshalNonNullableValue(t *testing.T) { + p1 := JsonPatchOperation{ + Operation: "remove", + Path: "/a1", + } + assert.JSONEq(t, `{"op":"remove", "path":"/a1"}`, p1.Json()) + +} diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch_simple_test.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_simple_test.go new file mode 100644 index 0000000000..1a6ec29721 --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_simple_test.go @@ -0,0 +1,120 @@ +package jsonpatch + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +var simpleA = `{"a":100, "b":200, "c":"hello"}` +var simpleB = `{"a":100, "b":200, "c":"goodbye"}` +var simpleC = `{"a":100, "b":100, "c":"hello"}` +var simpleD = `{"a":100, "b":200, "c":"hello", "d":"foo"}` +var simpleE = `{"a":100, "b":200}` +var simplef = `{"a":100, "b":100, "d":"foo"}` +var simpleG = `{"a":100, "b":null, "d":"foo"}` +var empty = `{}` + +func TestOneNullReplace(t *testing.T) { + patch, e := CreatePatch([]byte(simplef), []byte(simpleG)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 1, "they should be equal") + change := patch[0] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/b", "they should be equal") + assert.Equal(t, change.Value, nil, "they should be equal") +} + +func TestSame(t *testing.T) { + patch, e := CreatePatch([]byte(simpleA), []byte(simpleA)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 0, "they should be equal") +} + +func TestOneStringReplace(t *testing.T) { + patch, e := CreatePatch([]byte(simpleA), []byte(simpleB)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 1, "they should be equal") + change := patch[0] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/c", "they should be equal") + assert.Equal(t, change.Value, "goodbye", "they should be equal") +} + +func TestOneIntReplace(t *testing.T) { + patch, e := CreatePatch([]byte(simpleA), []byte(simpleC)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 1, "they should be equal") + change := patch[0] + assert.Equal(t, change.Operation, "replace", "they should be equal") + assert.Equal(t, change.Path, "/b", "they should be equal") + var expected float64 = 100 + assert.Equal(t, change.Value, expected, "they should be equal") +} + +func TestOneAdd(t *testing.T) { + patch, e := CreatePatch([]byte(simpleA), []byte(simpleD)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 1, "they should be equal") + change := patch[0] + assert.Equal(t, change.Operation, "add", "they should be equal") + assert.Equal(t, change.Path, "/d", "they should be equal") + assert.Equal(t, change.Value, "foo", "they should be equal") +} + +func TestOneRemove(t *testing.T) { + patch, e := CreatePatch([]byte(simpleA), []byte(simpleE)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 1, "they should be equal") + change := patch[0] + assert.Equal(t, change.Operation, "remove", "they should be equal") + assert.Equal(t, change.Path, "/c", "they should be equal") + assert.Equal(t, change.Value, nil, "they should be equal") +} + +func TestVsEmpty(t *testing.T) { + patch, e := CreatePatch([]byte(simpleA), []byte(empty)) + assert.NoError(t, e) + assert.Equal(t, len(patch), 3, "they should be equal") + sort.Sort(ByPath(patch)) + change := patch[0] + assert.Equal(t, change.Operation, "remove", "they should be equal") + assert.Equal(t, change.Path, "/a", "they should be equal") + + change = patch[1] + assert.Equal(t, change.Operation, "remove", "they should be equal") + assert.Equal(t, change.Path, "/b", "they should be equal") + + change = patch[2] + assert.Equal(t, change.Operation, "remove", "they should be equal") + assert.Equal(t, change.Path, "/c", "they should be equal") +} + +func BenchmarkBigArrays(b *testing.B) { + var a1, a2 []interface{} + a1 = make([]interface{}, 100) + a2 = make([]interface{}, 101) + + for i := 0; i < 100; i++ { + a1[i] = i + a2[i+1] = i + } + for i := 0; i < b.N; i++ { + compareArray(a1, a2, "/") + } +} + +func BenchmarkBigArrays2(b *testing.B) { + var a1, a2 []interface{} + a1 = make([]interface{}, 100) + a2 = make([]interface{}, 101) + + for i := 0; i < 100; i++ { + a1[i] = i + a2[i] = i + } + for i := 0; i < b.N; i++ { + compareArray(a1, a2, "/") + } +} diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch_supercomplex_test.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_supercomplex_test.go new file mode 100644 index 0000000000..6e98f3a6ab --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch_supercomplex_test.go @@ -0,0 +1,506 @@ +package jsonpatch + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +var superComplexBase = ` +{ + "annotations": { + "annotation": [ + { + "name": "version", + "value": "8" + }, + { + "name": "versionTag", + "value": "Published on May 13, 2015 at 8:48pm (MST)" + } + ] + }, + "attributes": { + "attribute-key": [ + { + "id": "3b05c943-d81a-436f-b242-8b519e7a6f30", + "properties": { + "visible": true + } + }, + { + "id": "d794c7ee-2a4b-4da4-bba7-e8b973d50c4b", + "properties": { + "visible": true + } + }, + { + "id": "a0259458-517c-480f-9f04-9b54b1b2af1f", + "properties": { + "visible": true + } + }, + { + "id": "9415f39d-c396-4458-9019-fc076c847964", + "properties": { + "visible": true + } + }, + { + "id": "0a2e49a9-8989-42fb-97da-cc66334f828b", + "properties": { + "visible": true + } + }, + { + "id": "27f5f14a-ea97-4feb-b22a-6ff754a31212", + "properties": { + "visible": true + } + }, + { + "id": "6f810508-4615-4fd0-9e87-80f9c94f9ad8", + "properties": { + "visible": true + } + }, + { + "id": "3451b1b2-7365-455c-8bb1-0b464d4d3ba1", + "properties": { + "visible": true + } + }, + { + "id": "a82ec957-8c26-41ea-8af6-6dd75c384801", + "properties": { + "visible": true + } + }, + { + "id": "736c5496-9a6e-4a82-aa00-456725796432", + "properties": { + "visible": true + } + }, + { + "id": "2d428b3c-9d3b-4ec1-bf98-e00673599d60", + "properties": { + "visible": true + } + }, + { + "id": "68566ebb-811d-4337-aba9-a8a8baf90e4b", + "properties": { + "visible": true + } + }, + { + "id": "ca88bab1-a1ea-40cc-8f96-96d1e9f1217d", + "properties": { + "visible": true + } + }, + { + "id": "c63a12c8-542d-47f3-bee1-30b5fe2b0690", + "properties": { + "visible": true + } + }, + { + "id": "cbd9e3bc-6a49-432a-a906-b1674c1de24c", + "properties": { + "visible": true + } + }, + { + "id": "03262f07-8a15-416d-a3f5-e2bf561c78f9", + "properties": { + "visible": true + } + }, + { + "id": "e5c93b87-83fc-45b6-b4d5-bf1e3f523075", + "properties": { + "visible": true + } + }, + { + "id": "72260ac5-3d51-49d7-bb31-f794dd129f1c", + "properties": { + "visible": true + } + }, + { + "id": "d856bde1-1b42-4935-9bee-c37e886c9ecf", + "properties": { + "visible": true + } + }, + { + "id": "62380509-bedf-4134-95c3-77ff377a4a6a", + "properties": { + "visible": true + } + }, + { + "id": "f4ed5ac9-b386-49a6-a0a0-6f3341ce9021", + "properties": { + "visible": true + } + }, + { + "id": "528d2bd2-87fe-4a49-954a-c93a03256929", + "properties": { + "visible": true + } + }, + { + "id": "ff8951f1-61a7-416b-9223-fac4bb6dac50", + "properties": { + "visible": true + } + }, + { + "id": "95c2b011-d782-4042-8a07-6aa4a5765c2e", + "properties": { + "visible": true + } + }, + { + "id": "dbe5837b-0624-4a05-91f3-67b5bd9b812a", + "properties": { + "visible": true + } + }, + { + "id": "13f198ed-82ab-4e51-8144-bfaa5bf77fd5", + "properties": { + "visible": true + } + }, + { + "id": "025312eb-12b6-47e6-9750-0fb31ddc2111", + "properties": { + "visible": true + } + }, + { + "id": "24292d58-db66-4ef3-8f4f-005d7b719433", + "properties": { + "visible": true + } + }, + { + "id": "22e5b5c4-821c-413a-a5b1-ab866d9a03bb", + "properties": { + "visible": true + } + }, + { + "id": "2fde0aac-df89-403d-998e-854b949c7b57", + "properties": { + "visible": true + } + }, + { + "id": "8b576876-5c16-4178-805e-24984c24fac3", + "properties": { + "visible": true + } + }, + { + "id": "415b7d2a-b362-4f1e-b83a-927802328ecb", + "properties": { + "visible": true + } + }, + { + "id": "8ef24fc2-ab25-4f22-9d9f-61902b49dc01", + "properties": { + "visible": true + } + }, + { + "id": "2299b09e-9f8e-4b79-a55c-a7edacde2c85", + "properties": { + "visible": true + } + }, + { + "id": "bf506538-f438-425c-be85-5aa2f9b075b8", + "properties": { + "visible": true + } + }, + { + "id": "2b501dc6-799d-4675-9144-fac77c50c57c", + "properties": { + "visible": true + } + }, + { + "id": "c0446da1-e069-417e-bd5a-34edcd028edc", + "properties": { + "visible": true + } + } + ] + } +}` + +var superComplexA = ` +{ + "annotations": { + "annotation": [ + { + "name": "version", + "value": "8" + }, + { + "name": "versionTag", + "value": "Published on May 13, 2015 at 8:48pm (MST)" + } + ] + }, + "attributes": { + "attribute-key": [ + { + "id": "3b05c943-d81a-436f-b242-8b519e7a6f30", + "properties": { + "visible": true + } + }, + { + "id": "d794c7ee-2a4b-4da4-bba7-e8b973d50c4b", + "properties": { + "visible": true + } + }, + { + "id": "a0259458-517c-480f-9f04-9b54b1b2af1f", + "properties": { + "visible": true + } + }, + { + "id": "9415f39d-c396-4458-9019-fc076c847964", + "properties": { + "visible": true + } + }, + { + "id": "0a2e49a9-8989-42fb-97da-cc66334f828b", + "properties": { + "visible": true + } + }, + { + "id": "27f5f14a-ea97-4feb-b22a-6ff754a31212", + "properties": { + "visible": true + } + }, + { + "id": "6f810508-4615-4fd0-9e87-80f9c94f9ad8", + "properties": { + "visible": true + } + }, + { + "id": "3451b1b2-7365-455c-8bb1-0b464d4d3ba1", + "properties": { + "visible": true + } + }, + { + "id": "a82ec957-8c26-41ea-8af6-6dd75c384801", + "properties": { + "visible": true + } + }, + { + "id": "736c5496-9a6e-4a82-aa00-456725796432", + "properties": { + "visible": true + } + }, + { + "id": "2d428b3c-9d3b-4ec1-bf98-e00673599d60", + "properties": { + "visible": true + } + }, + { + "id": "68566ebb-811d-4337-aba9-a8a8baf90e4b", + "properties": { + "visible": true + } + }, + { + "id": "ca88bab1-a1ea-40cc-8f96-96d1e9f1217d", + "properties": { + "visible": true + } + }, + { + "id": "c63a12c8-542d-47f3-bee1-30b5fe2b0690", + "properties": { + "visible": true + } + }, + { + "id": "cbd9e3bc-6a49-432a-a906-b1674c1de24c", + "properties": { + "visible": true + } + }, + { + "id": "03262f07-8a15-416d-a3f5-e2bf561c78f9", + "properties": { + "visible": true + } + }, + { + "id": "e5c93b87-83fc-45b6-b4d5-bf1e3f523075", + "properties": { + "visible": true + } + }, + { + "id": "72260ac5-3d51-49d7-bb31-f794dd129f1c", + "properties": { + "visible": true + } + }, + { + "id": "d856bde1-1b42-4935-9bee-c37e886c9ecf", + "properties": { + "visible": true + } + }, + { + "id": "62380509-bedf-4134-95c3-77ff377a4a6a", + "properties": { + "visible": true + } + }, + { + "id": "f4ed5ac9-b386-49a6-a0a0-6f3341ce9021", + "properties": { + "visible": true + } + }, + { + "id": "528d2bd2-87fe-4a49-954a-c93a03256929", + "properties": { + "visible": true + } + }, + { + "id": "ff8951f1-61a7-416b-9223-fac4bb6dac50", + "properties": { + "visible": true + } + }, + { + "id": "95c2b011-d782-4042-8a07-6aa4a5765c2e", + "properties": { + "visible": true + } + }, + { + "id": "dbe5837b-0624-4a05-91f3-67b5bd9b812a", + "properties": { + "visible": true + } + }, + { + "id": "13f198ed-82ab-4e51-8144-bfaa5bf77fd5", + "properties": { + "visible": true + } + }, + { + "id": "025312eb-12b6-47e6-9750-0fb31ddc2111", + "properties": { + "visible": true + } + }, + { + "id": "24292d58-db66-4ef3-8f4f-005d7b719433", + "properties": { + "visible": true + } + }, + { + "id": "22e5b5c4-821c-413a-a5b1-ab866d9a03bb", + "properties": { + "visible": true + } + }, + { + "id": "2fde0aac-df89-403d-998e-854b949c7b57", + "properties": { + "visible": true + } + }, + { + "id": "8b576876-5c16-4178-805e-24984c24fac3", + "properties": { + "visible": true + } + }, + { + "id": "415b7d2a-b362-4f1e-b83a-927802328ecb", + "properties": { + "visible": true + } + }, + { + "id": "8ef24fc2-ab25-4f22-9d9f-61902b49dc01", + "properties": { + "visible": true + } + }, + { + "id": "2299b09e-9f8e-4b79-a55c-a7edacde2c85", + "properties": { + "visible": true + } + }, + { + "id": "bf506538-f438-425c-be85-5aa2f9b075b8", + "properties": { + "visible": true + } + }, + { + "id": "2b501dc6-799d-4675-9144-fac77c50c57c", + "properties": { + "visible": true + } + }, + { + "id": "c0446da1-e069-417e-bd5a-34edcd028edc", + "properties": { + "visible": false + } + } + ] + } +}` + +func TestSuperComplexSame(t *testing.T) { + patch, e := CreatePatch([]byte(superComplexBase), []byte(superComplexBase)) + assert.NoError(t, e) + assert.Equal(t, 0, len(patch), "they should be equal") +} + +func TestSuperComplexBoolReplace(t *testing.T) { + patch, e := CreatePatch([]byte(superComplexBase), []byte(superComplexA)) + assert.NoError(t, e) + assert.Equal(t, 1, len(patch), "they should be equal") + change := patch[0] + assert.Equal(t, "replace", change.Operation, "they should be equal") + assert.Equal(t, "/attributes/attribute-key/36/properties/visible", change.Path, "they should be equal") + assert.Equal(t, false, change.Value, "they should be equal") +} diff --git a/vendor/golang.org/x/crypto/.gitattributes b/vendor/golang.org/x/crypto/.gitattributes new file mode 100644 index 0000000000..d2f212e5da --- /dev/null +++ b/vendor/golang.org/x/crypto/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files in this repo as binary, with no git magic updating +# line endings. Windows users contributing to Go will need to use a +# modern version of git and editors capable of LF line endings. +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-review gofmt checks. +# +# See golang.org/issue/9281 + +* -text diff --git a/vendor/golang.org/x/net/.gitattributes b/vendor/golang.org/x/net/.gitattributes new file mode 100644 index 0000000000..d2f212e5da --- /dev/null +++ b/vendor/golang.org/x/net/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files in this repo as binary, with no git magic updating +# line endings. Windows users contributing to Go will need to use a +# modern version of git and editors capable of LF line endings. +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-review gofmt checks. +# +# See golang.org/issue/9281 + +* -text diff --git a/vendor/golang.org/x/sys/.gitattributes b/vendor/golang.org/x/sys/.gitattributes new file mode 100644 index 0000000000..d2f212e5da --- /dev/null +++ b/vendor/golang.org/x/sys/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files in this repo as binary, with no git magic updating +# line endings. Windows users contributing to Go will need to use a +# modern version of git and editors capable of LF line endings. +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-review gofmt checks. +# +# See golang.org/issue/9281 + +* -text diff --git a/vendor/golang.org/x/text/.gitattributes b/vendor/golang.org/x/text/.gitattributes new file mode 100644 index 0000000000..d2f212e5da --- /dev/null +++ b/vendor/golang.org/x/text/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files in this repo as binary, with no git magic updating +# line endings. Windows users contributing to Go will need to use a +# modern version of git and editors capable of LF line endings. +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-review gofmt checks. +# +# See golang.org/issue/9281 + +* -text diff --git a/vendor/k8s.io/client-go/pkg/version/.gitattributes b/vendor/k8s.io/client-go/pkg/version/.gitattributes new file mode 100644 index 0000000000..7e349eff60 --- /dev/null +++ b/vendor/k8s.io/client-go/pkg/version/.gitattributes @@ -0,0 +1 @@ +base.go export-subst