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

chore: change v2alpha1 SubNamespace status to kstatus-compatible subresource #96

Merged
merged 1 commit into from
Nov 8, 2023
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
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(),
)
}
58 changes: 58 additions & 0 deletions api/v1/subnamespace_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package v1

import (
"testing"

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"
)

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