Skip to content

Commit

Permalink
Merge pull request #160 from fluxcd/ssa-fix
Browse files Browse the repository at this point in the history
Fix SSA upstream bugs for Kubernetes < 1.22
  • Loading branch information
stefanprodan authored Oct 10, 2021
2 parents 6c1a229 + 4b0783c commit 7cdf669
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
1 change: 1 addition & 0 deletions ssa/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.16

require (
github.com/google/go-cmp v0.5.6
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
// pin cli-utils to avoid kustomize@v2.0.3+incompatible dependency in v0.25.0
sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a
Expand Down
26 changes: 26 additions & 0 deletions ssa/manager_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,29 @@ func TestApply(t *testing.T) {
}
})
}

func TestApply_SetNativeKindsDefaults(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

id := generateName("fix")
objects, err := readManifest("testdata/test2.yaml", id)
if err != nil {
t.Fatal(err)
}

manager.SetOwnerLabels(objects, "app1", "default")

if err := SetNativeKindsDefaults(objects); err != nil {
t.Fatal(err)
}

t.Run("creates objects", func(t *testing.T) {
// create objects
_, err := manager.ApplyAllStaged(ctx, objects, false, timeout)
if err != nil {
t.Fatal(err)
}
})
}
102 changes: 102 additions & 0 deletions ssa/testdata/test2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
apiVersion: v1
kind: Namespace
metadata:
name: "%[1]s"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "%[1]s"
namespace: "%[1]s"
spec:
minReadySeconds: 3
revisionHistoryLimit: 5
progressDeadlineSeconds: 60
strategy:
rollingUpdate:
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
app: "%[1]s"
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9797"
labels:
app: "%[1]s"
spec:
containers:
- name: podinfod
image: ghcr.io/stefanprodan/podinfo:6.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 9898
# associative list with keys has an element that omits key field "protocol"
#protocol: TCP
- name: http-metrics
containerPort: 9797
#protocol: TCP
- name: grpc
containerPort: 9999
#protocol: TCP
command:
- ./podinfo
- --port=9898
- --port-metrics=9797
- --grpc-port=9999
- --grpc-service-name=podinfo
- --level=info
- --random-delay=false
- --random-error=false
env:
- name: PODINFO_UI_COLOR
value: "#34577c"
livenessProbe:
exec:
command:
- podcli
- check
- http
- localhost:9898/healthz
initialDelaySeconds: 5
timeoutSeconds: 5
readinessProbe:
exec:
command:
- podcli
- check
- http
- localhost:9898/readyz
initialDelaySeconds: 5
timeoutSeconds: 5
resources:
limits:
# expected string, got &value.valueUnstructured{Value:2}
cpu: 2
memory: 512Mi
requests:
cpu: 1
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
name: "%[1]s"
namespace: "%[1]s"
spec:
type: ClusterIP
selector:
app: "%[1]s"
ports:
- name: http
port: 9898
# .spec.ports: element 0: associative list with keys has an element that omits key field "protocol"
#protocol: TCP
targetPort: http
- port: 9999
targetPort: grpc
#protocol: TCP
name: grpc
131 changes: 131 additions & 0 deletions ssa/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ package ssa

import (
"encoding/json"
"fmt"
"io"
"strings"

appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
apiruntime "k8s.io/apimachinery/pkg/runtime"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/cli-utils/pkg/object"
Expand Down Expand Up @@ -208,3 +213,129 @@ func isImmutableError(err error) bool {
}
return false
}

// SetNativeKindsDefaults implements workarounds for server-side apply upstream bugs affecting Kubernetes < 1.22
// ContainerPort missing default TCP proto: https://github.com/kubernetes-sigs/structured-merge-diff/issues/130
// ServicePort missing default TCP proto: https://github.com/kubernetes/kubernetes/pull/98576
// PodSpec resources missing int to string conversion for e.g. 'cpu: 2'
func SetNativeKindsDefaults(objects []*unstructured.Unstructured) error {

var setProtoDefault = func(spec *corev1.PodSpec) {
for _, c := range spec.Containers {
for i, port := range c.Ports {
if port.Protocol == "" {
c.Ports[i].Protocol = "TCP"
}
}
}
}

for _, u := range objects {
switch u.GetKind() {
case "Pod":
var d corev1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}

setProtoDefault(&d.Spec)

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
case "Deployment":
var d appsv1.Deployment
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}

setProtoDefault(&d.Spec.Template.Spec)

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
case "StatefulSet":
var d appsv1.StatefulSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}

setProtoDefault(&d.Spec.Template.Spec)

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
case "DaemonSet":
var d appsv1.DaemonSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}

setProtoDefault(&d.Spec.Template.Spec)

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
case "ReplicaSet":
var d appsv1.ReplicaSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}

setProtoDefault(&d.Spec.Template.Spec)

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
case "Service":
var d corev1.Service
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}

// set port protocol default
// workaround for: https://github.com/kubernetes-sigs/structured-merge-diff/issues/130
for i, port := range d.Spec.Ports {
if port.Protocol == "" {
d.Spec.Ports[i].Protocol = "TCP"
}
}

out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
case "HorizontalPodAutoscaler":
if strings.Contains(u.GetAPIVersion(), "autoscaling/v2") {
var d autoscalingv2.HorizontalPodAutoscaler
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&d)
if err != nil {
return fmt.Errorf("%s validation error: %w", FmtUnstructured(u), err)
}
u.Object = out
}
}
}
return nil
}

0 comments on commit 7cdf669

Please sign in to comment.