Skip to content

Commit

Permalink
make ScopeTemplate CRs communicate state (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
everettraven authored Oct 11, 2022
1 parent ec3031c commit 9665f86
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 9 deletions.
10 changes: 10 additions & 0 deletions api/v1alpha1/scopetemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,18 @@ type ClusterRoleTemplate struct {
type ScopeTemplateStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file

// Conditions represent the latest available observations of an object's state
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
}

const (
TypeTemplated = "Templated"

ReasonTemplatingFailed = "TemplatingFailed"
ReasonTemplatingSuccessful = "TemplatingSuccessful"
)

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster
Expand Down
10 changes: 9 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,77 @@ spec:
type: object
status:
description: ScopeTemplateStatus defines the observed state of ScopeTemplate
properties:
conditions:
description: Conditions represent the latest available observations
of an object's state
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
type FooStatus struct{ // Represents the observations of a foo's
current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
type: object
type: object
served: true
Expand Down
25 changes: 25 additions & 0 deletions controllers/scopetemplate_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/equality"
k8sapierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -111,6 +112,7 @@ func (r *ScopeTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Reques
func (r *ScopeTemplateReconciler) reconcile(ctx context.Context, st *operatorsv1.ScopeTemplate) (ctrl.Result, error) {
scopeinstances := operatorsv1.ScopeInstanceList{}
if err := r.Client.List(ctx, &scopeinstances, &client.ListOptions{}); err != nil {
updateStatusTemplatingFailed(st, err)
return ctrl.Result{}, err
}

Expand All @@ -121,19 +123,22 @@ func (r *ScopeTemplateReconciler) reconcile(ctx context.Context, st *operatorsv1
// create ClusterRoles based on the ScopeTemplate
log.Log.Info("ScopeInstance found that references ScopeTemplate", "name", st.Name)
if err := r.ensureClusterRoles(ctx, st); err != nil {
updateStatusTemplatingFailed(st, err)
return ctrl.Result{}, fmt.Errorf("creating ClusterRoles: %v", err)
}
}

// Add requirement to delete old (Cluster)Roles
stHashReq, err := labels.NewRequirement(scopeTemplateHashKey, selection.NotEquals, []string{util.HashObject(st.Spec)})
if err != nil {
updateStatusTemplatingFailed(st, err)
return ctrl.Result{}, err
}

// Only look for old (Cluster)Roles that map to this ScopeTemplate UID
stUIDReq, err := labels.NewRequirement(scopeTemplateUIDKey, selection.Equals, []string{string(st.GetUID())})
if err != nil {
updateStatusTemplatingFailed(st, err)
return ctrl.Result{}, err
}

Expand All @@ -142,9 +147,11 @@ func (r *ScopeTemplateReconciler) reconcile(ctx context.Context, st *operatorsv1
}

if err := r.deleteClusterRoles(ctx, listOptions); err != nil {
updateStatusTemplatingFailed(st, err)
return ctrl.Result{}, err
}

updateStatusTemplatingSuccessful(st, fmt.Sprintf("ScopeTemplate %q successfully reconciled", st.Name))
log.Log.Info("No ScopeTemplate error")
return ctrl.Result{}, nil
}
Expand Down Expand Up @@ -281,3 +288,21 @@ func (r *ScopeTemplateReconciler) clusterRoleManifest(crt *operatorsv1.ClusterRo
}
return cr
}

func updateStatusTemplatingFailed(st *operatorsv1.ScopeTemplate, err error) {
meta.SetStatusCondition(&st.Status.Conditions, metav1.Condition{
Type: operatorsv1.TypeTemplated,
Status: metav1.ConditionFalse,
Reason: operatorsv1.ReasonTemplatingFailed,
Message: err.Error(),
})
}

func updateStatusTemplatingSuccessful(st *operatorsv1.ScopeTemplate, msg string) {
meta.SetStatusCondition(&st.Status.Conditions, metav1.Condition{
Type: operatorsv1.TypeTemplated,
Status: metav1.ConditionTrue,
Reason: operatorsv1.ReasonTemplatingSuccessful,
Message: msg,
})
}
66 changes: 58 additions & 8 deletions controllers/scopetemplate_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
Expand All @@ -41,9 +42,14 @@ var _ = Describe("ScopeTemplate", func() {
namespace2 *corev1.Namespace
scopeTemplate *operatorsv1.ScopeTemplate
scopeInstance *operatorsv1.ScopeInstance
// Use this to track the test iteration
iteration int

reasonSuccessMsgTemplate = "ScopeTemplate %q successfully reconciled"
)

BeforeEach(func() {
iteration += 1
namespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-",
Expand All @@ -69,7 +75,9 @@ var _ = Describe("ScopeTemplate", func() {
Spec: operatorsv1.ScopeTemplateSpec{
ClusterRoles: []operatorsv1.ClusterRoleTemplate{
{
GenerateName: "test",
// Create a new ClusterRole for each test iteration so that there are no conflicts with a ClusterRole already existing.
// We can deliberately test this scenario in specific tests in the future.
GenerateName: fmt.Sprintf("test-%d", iteration),
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{
Expand Down Expand Up @@ -107,6 +115,11 @@ var _ = Describe("ScopeTemplate", func() {

clusterRoleList := listClusterRole(0, labels)
Expect(clusterRoleList.Items).Should(BeNil())
verifyScopeTemplateStatus(scopeTemplate,
operatorsv1.TypeTemplated,
operatorsv1.ReasonTemplatingSuccessful,
metav1.ConditionTrue,
fmt.Sprintf(reasonSuccessMsgTemplate, scopeTemplate.Name))
})

When("a scopeInstance is created that references the scopeTemplate with a single namespace", func() {
Expand Down Expand Up @@ -157,17 +170,27 @@ var _ = Describe("ScopeTemplate", func() {
Resources: []string{"secrets"},
},
}))
verifyScopeTemplateStatus(scopeTemplate,
operatorsv1.TypeTemplated,
operatorsv1.ReasonTemplatingSuccessful,
metav1.ConditionTrue,
fmt.Sprintf(reasonSuccessMsgTemplate, scopeTemplate.Name))
})

It("should create the expected RoleBinding within the test namespace", func() {
labels := map[string]string{scopeInstanceUIDKey: string(scopeInstance.GetUID()),
clusterRoleBindingGenerateKey: "test"}
clusterRoleBindingGenerateKey: scopeTemplate.Spec.ClusterRoles[0].GenerateName}

roleBindingList := listRoleBinding(namespace.GetName(), 1, labels)

existingRB := &roleBindingList.Items[0]

verifyRoleBindings(existingRB, scopeInstance, scopeTemplate)
verifyScopeTemplateStatus(scopeTemplate,
operatorsv1.TypeTemplated,
operatorsv1.ReasonTemplatingSuccessful,
metav1.ConditionTrue,
fmt.Sprintf(reasonSuccessMsgTemplate, scopeTemplate.Name))
})

When("a scopeInstance is updated to include another namespace", func() {
Expand Down Expand Up @@ -198,7 +221,7 @@ var _ = Describe("ScopeTemplate", func() {
}, timeout, interval).Should(BeNil())

labels := map[string]string{scopeInstanceUIDKey: string(scopeInstance.GetUID()),
clusterRoleBindingGenerateKey: "test"}
clusterRoleBindingGenerateKey: scopeTemplate.Spec.ClusterRoles[0].GenerateName}

roleBindingList := listRoleBinding(namespace2.GetName(), 1, labels)

Expand All @@ -210,6 +233,11 @@ var _ = Describe("ScopeTemplate", func() {

existingRB = &roleBindingList.Items[0]
verifyRoleBindings(existingRB, scopeInstance, scopeTemplate)
verifyScopeTemplateStatus(scopeTemplate,
operatorsv1.TypeTemplated,
operatorsv1.ReasonTemplatingSuccessful,
metav1.ConditionTrue,
fmt.Sprintf(reasonSuccessMsgTemplate, scopeTemplate.Name))
})

When("a scopeInstance is updated to remove one of the namespace", func() {
Expand All @@ -227,7 +255,7 @@ var _ = Describe("ScopeTemplate", func() {
}, timeout, interval).Should(BeNil())

labels := map[string]string{scopeInstanceUIDKey: string(scopeInstance.GetUID()),
clusterRoleBindingGenerateKey: "test"}
clusterRoleBindingGenerateKey: scopeTemplate.Spec.ClusterRoles[0].GenerateName}

roleBindingList := listRoleBinding(namespace2.GetName(), 1, labels)

Expand Down Expand Up @@ -258,7 +286,7 @@ var _ = Describe("ScopeTemplate", func() {
err := k8sClient.List(ctx, clusterRoleBindingList,
client.MatchingLabels{
scopeInstanceUIDKey: string(scopeInstance.GetUID()),
clusterRoleBindingGenerateKey: "test",
clusterRoleBindingGenerateKey: scopeTemplate.Spec.ClusterRoles[0].GenerateName,
})
if err != nil {
return err
Expand Down Expand Up @@ -291,18 +319,23 @@ var _ = Describe("ScopeTemplate", func() {
}))
Expect(existingCRB.RoleRef).To(Equal(rbacv1.RoleRef{
Kind: "ClusterRole",
Name: "test",
Name: scopeTemplate.Spec.ClusterRoles[0].GenerateName,
APIGroup: "rbac.authorization.k8s.io",
}))

labels := map[string]string{scopeInstanceUIDKey: string(scopeInstance.GetUID()),
clusterRoleBindingGenerateKey: "test"}
clusterRoleBindingGenerateKey: scopeTemplate.Spec.ClusterRoles[0].GenerateName}

roleBindingList := listRoleBinding(namespace.GetName(), 0, labels)
Expect(len(roleBindingList.Items)).To(Equal(0))

roleBindingList = listRoleBinding(namespace2.GetName(), 0, labels)
Expect(len(roleBindingList.Items)).To(Equal(0))
verifyScopeTemplateStatus(scopeTemplate,
operatorsv1.TypeTemplated,
operatorsv1.ReasonTemplatingSuccessful,
metav1.ConditionTrue,
fmt.Sprintf(reasonSuccessMsgTemplate, scopeTemplate.Name))
})
})
})
Expand Down Expand Up @@ -331,11 +364,28 @@ func verifyRoleBindings(existingRB *rbacv1.RoleBinding, si *operatorsv1.ScopeIns
}))
Expect(existingRB.RoleRef).To(Equal(rbacv1.RoleRef{
Kind: "ClusterRole",
Name: "test",
Name: st.Spec.ClusterRoles[0].GenerateName,
APIGroup: "rbac.authorization.k8s.io",
}))
}

func verifyScopeTemplateStatus(st *operatorsv1.ScopeTemplate, conditionType string, reason string, status metav1.ConditionStatus, message string) {
tempSt := &operatorsv1.ScopeTemplate{}
Eventually(func() error {
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(st), tempSt)).NotTo(HaveOccurred())
if len(tempSt.Status.Conditions) == 0 {
return fmt.Errorf("status.conditions len not > 0")
}
return nil
}).Should(Succeed())

cond := meta.FindStatusCondition(tempSt.Status.Conditions, conditionType)
Expect(cond).ShouldNot(BeNil())
Expect(cond.Reason).Should(Equal(reason))
Expect(cond.Status).Should(Equal(status))
Expect(cond.Message).Should(Equal(message))
}

func listRoleBinding(namespace string, numberOfExpectedRoleBindings int, labels map[string]string) *rbacv1.RoleBindingList {
roleBindingList := &rbacv1.RoleBindingList{}
Eventually(func() error {
Expand Down

0 comments on commit 9665f86

Please sign in to comment.