Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: kustomize rollout: add openapi to doc and examples #1371

Merged
merged 4 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions docs/features/kustomize.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,69 @@ apiVersion: kustomize.config.k8s.io/v1beta1
configurations:
- rollout-transform.yaml
```
With Kustomize 3.6.1 it is possible to reference the configuration directly from a remote resource:

An example kustomize app demonstrating the ability to use transformers with Rollouts can be seen
[here](https://github.com/argoproj/argo-rollouts/blob/master/docs/features/kustomize/example).

- With Kustomize 3.6.1 it is possible to reference the configuration directly from a remote resource:

```yaml
configurations:
- https://argoproj.github.io/argo-rollouts/features/kustomize/rollout-transform.yaml
```

A example kustomize app demonstrating the ability to use transformers with Rollouts can be seen
[here](https://github.com/argoproj/argo-rollouts/blob/master/docs/features/kustomize/example).
- With Kustomize 4.1.0 kustomize can use kubernetes OpenAPI data to get merge key and patch strategy information about [resource types](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/openapi). For example, given the following rollout:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: rollout-canary
spec:
strategy:
canary:
steps:
# detail of the canary steps is omitted
template:
metadata:
labels:
app: rollout-canary
spec:
containers:
- name: rollouts-demo
image: argoproj/rollouts-demo:blue
imagePullPolicy: Always
ports:
- containerPort: 8080
```

user can update the Rollout via a patch in a kustomization file, to change the image to nginx

```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- rollout-canary.yaml

openapi:
path: <path-to-directory>/rollout_cr_schema.json

patchesStrategicMerge:
- |-
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: rollout-canary
spec:
template:
spec:
containers:
- name: rollouts-demo
image: nginx
```

The OpenAPI data is auto-generated and defined in this [file](https://github.com/argoproj/argo-rollouts/blob/master/docs/features/kustomize/rollout_cr_schema.json).

An example kustomize app demonstrating the ability to use OpenAPI data with Rollouts can be seen
[here](https://github.com/argoproj/argo-rollouts/blob/master/test/kustomize/rollout).
2,827 changes: 2,827 additions & 0 deletions docs/features/kustomize/rollout_cr_schema.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.0.0
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.6.1
github.com/blang/semver v3.5.1+incompatible
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
github.com/evanphx/json-patch/v5 v5.2.0
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
Expand Down
195 changes: 181 additions & 14 deletions hack/gen-crd-spec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"strings"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
unstructuredutil "github.com/argoproj/argo-rollouts/utils/unstructured"

"github.com/blang/semver"
"github.com/ghodss/yaml"
"github.com/go-openapi/spec"
extensionsobj "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

unstructuredutil "github.com/argoproj/argo-rollouts/utils/unstructured"
)

const metadataValidation = `properties:
Expand Down Expand Up @@ -228,18 +232,6 @@ func createMetadataValidation(un *unstructured.Unstructured) {
}
}

func removeFieldHelper(obj map[string]interface{}, fieldName string) {
for k, v := range obj {
if k == fieldName {
delete(obj, k)
continue
}
if vObj, ok := v.(map[string]interface{}); ok {
removeFieldHelper(vObj, fieldName)
}
}
}

func removeK8S118Fields(un *unstructured.Unstructured) {
kind := crdKind(un)
switch kind {
Expand Down Expand Up @@ -295,9 +287,184 @@ func checkErr(err error) {
}
}

// loadK8SDefinitions loads K8S types API schema definitions
func loadK8SDefinitions() (spec.Definitions, error) {
// detects minor version of k8s client
k8sVersionCmd := exec.Command("sh", "-c", "cat go.mod | grep \"k8s.io/client-go\" | head -n 1 | cut -d' ' -f2")
versionData, err := k8sVersionCmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to determine k8s client version: %v", err)
}
v, err := semver.Parse(strings.TrimSpace(strings.Replace(string(versionData), "v", "", 1)))
if err != nil {
return nil, err
}
resp, err := http.Get(fmt.Sprintf("https://raw.githubusercontent.com/kubernetes/kubernetes/release-1.%d/api/openapi-spec/swagger.json", v.Minor))
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
schema := spec.Schema{}
err = json.Unmarshal(data, &schema)
if err != nil {
return nil, err
}
return schema.Definitions, nil
}

// normalizeRef normalizes rollouts and k8s type references since they are slightly different:
// rollout refs are prefixed with #/definitions/ and k8s types refs starts with io.k8s instead of k8s.io and have no /
func normalizeRef(ref string) string {
if strings.HasPrefix(ref, "#/definitions/") {
ref = ref[len("#/definitions/"):]
}

if strings.HasPrefix(ref, "io.k8s.") {
ref = "k8s.io." + ref[len("io.k8s."):]
}
return strings.ReplaceAll(ref, "/", ".")
}

var patchAnnotationKeys = map[string]bool{
"x-kubernetes-patch-merge-key": true,
"x-kubernetes-patch-strategy": true,
"x-kubernetes-list-map-keys": true,
"x-kubernetes-list-type": true,
}

// injectPatchAnnotations injects patch annotations from given schema definitions and drop properties that don't have
// patch annotations injected
func injectPatchAnnotations(prop map[string]interface{}, propSchema spec.Schema, schemaDefinitions spec.Definitions) (bool, error) {
injected := false
for k, v := range propSchema.Extensions {
if patchAnnotationKeys[k] {
prop[k] = v
injected = true
}
}

var propSchemas map[string]spec.Schema
refStr := propSchema.Ref.String()
normalizedRef := normalizeRef(refStr)
switch {
case normalizedRef == "":
propSchemas = propSchema.Properties
default:
schema, ok := schemaDefinitions[normalizedRef]
if !ok {
return false, fmt.Errorf("not supported ref: %s", refStr)
}
propSchemas = schema.Properties
}

childProps, ok := prop["properties"].(map[string]interface{})
if !ok {
childProps = map[string]interface{}{}
}

for k, v := range childProps {
childInjected, err := injectPatchAnnotations(v.(map[string]interface{}), propSchemas[k], schemaDefinitions)
if err != nil {
return false, err
}
if !childInjected {
delete(childProps, k)
} else {
injected = true
childProps[k] = v
}
}
return injected, nil
}

const (
rolloutsDefinitionsPrefix = "github.com/argoproj/argo-rollouts/pkg/apis/rollouts"
)

// generateKustomizeSchema generates open api schema that has properties with patch annotations only
func generateKustomizeSchema(crds []*extensionsobj.CustomResourceDefinition, outputPath string) error {
k8sDefinitions, err := loadK8SDefinitions()
if err != nil {
return err
}
schemaDefinitions := map[string]spec.Schema{}
for k, v := range k8sDefinitions {
schemaDefinitions[normalizeRef(k)] = v
}

for k, v := range v1alpha1.GetOpenAPIDefinitions(func(path string) spec.Ref {
return spec.MustCreateRef(path)
}) {
schemaDefinitions[normalizeRef(k)] = v.Schema
}

definitions := map[string]interface{}{}
for _, crd := range crds {
if crd.Spec.Names.Kind != "Rollout" {
continue
}
var version string
var props map[string]extensionsobj.JSONSchemaProps
for _, v := range crd.Spec.Versions {
if v.Schema == nil || v.Schema.OpenAPIV3Schema == nil {
continue
}
version = v.Name
props = v.Schema.OpenAPIV3Schema.Properties
}

data, err := json.Marshal(props)
if err != nil {
return err
}
propsMap := map[string]interface{}{}
err = json.Unmarshal(data, &propsMap)
if err != nil {
return err
}

crdSchema := schemaDefinitions[normalizeRef(fmt.Sprintf("%s/%s.%s", rolloutsDefinitionsPrefix, version, crd.Spec.Names.Kind))]
for k, p := range propsMap {
injected, err := injectPatchAnnotations(p.(map[string]interface{}), crdSchema.Properties[k], schemaDefinitions)
if err != nil {
return err
}
if injected {
propsMap[k] = p
} else {
delete(propsMap, k)
}
}

definitions[fmt.Sprintf("%s.%s", version, crd.Spec.Names.Kind)] = map[string]interface{}{
"properties": propsMap,
"x-kubernetes-group-version-kind": []map[string]string{{
"group": crd.Spec.Group,
"kind": crd.Spec.Names.Kind,
"version": version,
}},
}
}
data, err := json.MarshalIndent(map[string]interface{}{
"definitions": definitions,
}, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(outputPath, data, 0644)
}

// Generate CRD spec for Rollout Resource
func main() {
crds := NewCustomResourceDefinition()

err := generateKustomizeSchema(crds, "docs/features/kustomize/rollout_cr_schema.json")
checkErr(err)

for i := range crds {
crd := crds[i]
crdKind := crd.Spec.Names.Kind
Expand Down
4 changes: 1 addition & 3 deletions manifests/cluster-install/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
resources:
- ../crds
- ../base
- ../role

resources:
- argo-rollouts-clusterrolebinding.yaml
2 changes: 1 addition & 1 deletion test/kustomize/rollout/expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ spec:
configMapKeyRef:
key: FOO
name: my-guestbook-cm-m2mg5mb749
image: guestbook:v2
image: guestbook-patched:v1
name: guestbook
ports:
- containerPort: 8080
Expand Down
16 changes: 16 additions & 0 deletions test/kustomize/rollout/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,19 @@ replicas:
images:
- name: guestbook
newTag: v2

openapi:
path: ../../../docs/features/kustomize/rollout_cr_schema.json

patchesStrategicMerge:
- |-
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: guestbook
spec:
template:
spec:
containers:
- name: guestbook
image: guestbook-patched:v1