Skip to content

Commit

Permalink
feat: kustomize rollout: add openapi to doc and examples (#1371)
Browse files Browse the repository at this point in the history
feat: kustomize rollout: add openapi to doc and examples (#1371)

- openAPI schema for kustomize rollout

Signed-off-by: Hui Kang <hui.kang@salesforce.com>

Co-authored-by: Hui Kang <hui.kang@salesforce.com>
Co-authored-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
  • Loading branch information
3 people authored Sep 3, 2021
1 parent 1353526 commit ba9af78
Show file tree
Hide file tree
Showing 7 changed files with 3,087 additions and 21 deletions.
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

0 comments on commit ba9af78

Please sign in to comment.