Skip to content

Commit

Permalink
chore: convert v2alpha1 SubNamespace status to kstatus-compatible sub…
Browse files Browse the repository at this point in the history
…resource
  • Loading branch information
erikgb committed Oct 27, 2023
1 parent 7fc21ce commit a69fa81
Show file tree
Hide file tree
Showing 13 changed files with 495 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ envtest: setup-envtest

.PHONY: test
test: test-tools
go test -v -count 1 -race ./pkg/...
go test -v -count 1 -race ./api/... ./pkg/...
go install ./...
go vet ./...
test -z $$(gofmt -s -l . | tee /dev/stderr)
Expand Down
34 changes: 34 additions & 0 deletions api/v1/conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package v1

import (
"testing"

accuratev2alpha1 "github.com/cybozu-go/accurate/api/v2alpha1"
utilconversion "github.com/cybozu-go/accurate/internal/util/conversion"
fuzz "github.com/google/gofuzz"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
)

func TestFuzzyConversion(t *testing.T) {
t.Run("for SubNamespace", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{
Hub: &accuratev2alpha1.SubNamespace{},
Spoke: &SubNamespace{},
FuzzerFuncs: []fuzzer.FuzzerFuncs{SubNamespaceStatusFuzzFunc},
}))
}

func SubNamespaceStatusFuzzFunc(_ runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
SubNamespaceStatusFuzzer,
}
}

func SubNamespaceStatusFuzzer(in *SubNamespace, c fuzz.Continue) {
c.FuzzNoCustom(in)

// The status is just a string in v1, and the controller is the sole actor updating status.
// As long as we make the controller reconcile v2alpha1, and also makes it the stored version,
// we will never need to convert status from v1 to v2alpha1.
in.Status = ""
}
108 changes: 108 additions & 0 deletions api/v1/subnamespace_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package v1

import (
"encoding/json"
"fmt"
"strconv"

accuratev2alpha1 "github.com/cybozu-go/accurate/api/v2alpha1"
"github.com/cybozu-go/accurate/pkg/constants"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/meta"
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/conversion"
)

// ConvertTo converts this SubNamespace to the Hub version (v2alpha1).
func (src *SubNamespace) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*accuratev2alpha1.SubNamespace)

logger := getConversionLogger(src).WithValues(
"source", GroupVersion.Version,
"destination", GroupVersion.Version,
)
logger.V(5).Info("converting")

dst.ObjectMeta = src.ObjectMeta
dst.Spec.Annotations = src.Spec.Annotations
dst.Spec.Labels = src.Spec.Labels

// Restore info from annotations to ensure conversions are lossy-less.
// Delete annotation after processing it to avoid polluting converted resource.
if v, ok := dst.Annotations[constants.AnnObservedGeneration]; ok {
obsGen, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("error converting %q to int64 from annotation %s", v, constants.AnnObservedGeneration)
}
dst.Status.ObservedGeneration = obsGen

delete(dst.Annotations, constants.AnnObservedGeneration)
}
if conds, ok := dst.Annotations[constants.AnnConditions]; ok {
err := json.Unmarshal([]byte(conds), &dst.Status.Conditions)
if err != nil {
return fmt.Errorf("error unmarshalling JSON from annotation %s", constants.AnnConditions)
}

delete(dst.Annotations, constants.AnnConditions)
}

return nil
}

// ConvertFrom converts from the Hub version (v2alpha1) to this version.
func (dst *SubNamespace) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*accuratev2alpha1.SubNamespace)

logger := getConversionLogger(src).WithValues(
"source", GroupVersion.Version,
"destination", GroupVersion.Version,
)
logger.V(5).Info("converting")

dst.ObjectMeta = src.ObjectMeta
dst.Spec.Annotations = src.Spec.Annotations
dst.Spec.Labels = src.Spec.Labels

switch {
case meta.IsStatusConditionTrue(src.Status.Conditions, string(kstatus.ConditionStalled)):
dst.Status = SubNamespaceConflict
case src.Status.ObservedGeneration == 0:
// SubNamespace has never been reconciled.
case src.Status.ObservedGeneration == src.Generation && len(src.Status.Conditions) == 0:
dst.Status = SubNamespaceOK
default:
// SubNamespace is in some transitional state, not possible to represent in v1 status.
// An unset value is probably our best option.
}

// Store info in annotations to ensure conversions are lossy-less.
if dst.Annotations == nil {
dst.Annotations = make(map[string]string)
}
if src.Status.ObservedGeneration != 0 {
dst.Annotations[constants.AnnObservedGeneration] = strconv.FormatInt(src.Status.ObservedGeneration, 10)
}
if len(src.Status.Conditions) > 0 {
buf, err := json.Marshal(src.Status.Conditions)
if err != nil {
return fmt.Errorf("error marshalling conditions to JSON")
}
dst.Annotations[constants.AnnConditions] = string(buf)
}
if len(dst.Annotations) == 0 {
dst.Annotations = nil
}

return nil
}

func getConversionLogger(obj client.Object) logr.Logger {
return ctrl.Log.WithName("conversion").WithValues(
"kind", "SubNamespace",
"namespace", obj.GetNamespace(),
"name", obj.GetName(),
)
}
57 changes: 57 additions & 0 deletions api/v1/subnamespace_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package v1

import (
accuratev2alpha1 "github.com/cybozu-go/accurate/api/v2alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
"testing"
)

func TestSubNamespace_ConvertFrom(t *testing.T) {
tests := map[string]struct {
src *accuratev2alpha1.SubNamespace
expStatus SubNamespaceStatus
wantErr bool
}{
"if SubNamespace has never been reconciled, status should have zero-value": {
src: newSubNamespaceWithStatus(0, 0),
},
"if SubNamespace spec is updated, but not yet reconciled, status should have zero-value": {
src: newSubNamespaceWithStatus(2, 1),
},
"if SubNamespace is reconciled successfully, status should be ok": {
src: newSubNamespaceWithStatus(1, 1),
expStatus: SubNamespaceOK,
},
"if SubNamespace is reconciled with errors, status should be conflict": {
src: newSubNamespaceWithStatus(1, 1, newStalledCondition()),
expStatus: SubNamespaceConflict,
},
}
for n, tt := range tests {
t.Run(n, func(t *testing.T) {
dst := &SubNamespace{}
if err := dst.ConvertFrom(tt.src); (err != nil) != tt.wantErr {
t.Errorf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr)
}
if dst.Status != tt.expStatus {
t.Errorf("ConvertFrom() status = %q, expStatus %q", dst.Status, tt.expStatus)
}
})
}
}

func newSubNamespaceWithStatus(gen, obsGen int, conds ...metav1.Condition) *accuratev2alpha1.SubNamespace {
subNS := &accuratev2alpha1.SubNamespace{}
subNS.Generation = int64(gen)
subNS.Status.ObservedGeneration = int64(obsGen)
subNS.Status.Conditions = conds
return subNS
}

func newStalledCondition() metav1.Condition {
return metav1.Condition{
Type: string(kstatus.ConditionStalled),
Status: metav1.ConditionTrue,
}
}
4 changes: 4 additions & 0 deletions api/v2alpha1/subnamespace_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package v2alpha1

// Hub marks this SubNamespace version as a conversion hub.
func (*SubNamespace) Hub() {}
17 changes: 11 additions & 6 deletions api/v2alpha1/subnamespace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import (
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// SubNamespaceStatus defines the observed state of SubNamespace
// +kubebuilder:validation:Enum=ok;conflict
type SubNamespaceStatus string
type SubNamespaceStatus struct {
// The generation observed by the object controller.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

const (
SubNamespaceOK = SubNamespaceStatus("ok")
SubNamespaceConflict = SubNamespaceStatus("conflict")
)
// Conditions represent the latest available observations of an object's state
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// SubNamespaceSpec defines the desired state of SubNamespace
type SubNamespaceSpec struct {
Expand All @@ -30,6 +34,7 @@ type SubNamespaceSpec struct {
// Keeping this version un-served for now
//+kubebuilder:unservedversion
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// SubNamespace is the Schema for the subnamespaces API
type SubNamespace struct {
Expand Down
24 changes: 24 additions & 0 deletions api/v2alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 55 additions & 4 deletions charts/accurate/crds/accurate.cybozu.com_subnamespaces.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a69fa81

Please sign in to comment.