Skip to content

Commit 37fca0b

Browse files
rammanojrpotla-akamAshleyDumaine
authored
[feat, breaking] add LMT controller to support tag propagation, remove tag propagation via annotations (#790)
* add LMT controller * remove local-dev files * clone tags in lm * auto-gen role * address feedback & set status tags only on success * add e2e test for LMT * remove redundant comment --------- Co-authored-by: rpotla <[email protected]> Co-authored-by: Ashley Dumaine <[email protected]>
1 parent fd3dc6d commit 37fca0b

27 files changed

+1117
-170
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ release/*
1010
templates/cluster-template*.yaml
1111
infrastructure-*-linode/*
1212
.envrc
13+
vendor/

api/v1alpha2/linodemachine_types.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ type LinodeMachineSpec struct {
6565
BackupsEnabled bool `json:"backupsEnabled,omitempty"`
6666
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
6767
PrivateIP *bool `json:"privateIP,omitempty"`
68-
// Deprecated: spec.tags is deprecated, use metadata.annotations.linode-vm-tags instead.
69-
// +kubebuilder:deprecatedversion:warning="spec.tags is deprecated, use metadata.annotations.linode-vm-tags instead"
70-
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
68+
// Tags is a list of tags to apply to the Linode instance.
7169
Tags []string `json:"tags,omitempty"`
7270
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
7371
FirewallID int `json:"firewallID,omitempty"`
@@ -225,6 +223,10 @@ type LinodeMachineStatus struct {
225223
// Conditions defines current service state of the LinodeMachine.
226224
// +optional
227225
Conditions []metav1.Condition `json:"conditions,omitempty"`
226+
227+
// tags are the tags applied to the Linode Machine.
228+
// +optional
229+
Tags []string `json:"tags,omitempty"`
228230
}
229231

230232
// +kubebuilder:object:root=true

api/v1alpha2/linodemachinetemplate_types.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ type LinodeMachineTemplateSpec struct {
2525
Template LinodeMachineTemplateResource `json:"template"`
2626
}
2727

28+
// LinodeMachineTemplateStatus defines the observed state of LinodeMachineTemplate
29+
// It is used to store the status of the LinodeMachineTemplate, such as tags.
30+
type LinodeMachineTemplateStatus struct {
31+
32+
// tags that are currently applied to the LinodeMachineTemplate.
33+
// +optional
34+
Tags []string `json:"tags,omitempty"`
35+
36+
// Conditions represent the latest available observations of a LinodeMachineTemplate's current state.
37+
// +optional
38+
Conditions []metav1.Condition `json:"conditions,omitempty"`
39+
}
40+
2841
// LinodeMachineTemplateResource describes the data needed to create a LinodeMachine from a template.
2942
type LinodeMachineTemplateResource struct {
3043
Spec LinodeMachineSpec `json:"spec"`
@@ -33,6 +46,7 @@ type LinodeMachineTemplateResource struct {
3346
// +kubebuilder:object:root=true
3447
// +kubebuilder:storageversion
3548
// +kubebuilder:resource:path=linodemachinetemplates,scope=Namespaced,categories=cluster-api,shortName=lmt
49+
// +kubebuilder:subresource:status
3650
// +kubebuilder:metadata:labels="clusterctl.cluster.x-k8s.io/move-hierarchy=true"
3751

3852
// LinodeMachineTemplate is the Schema for the linodemachinetemplates API
@@ -41,6 +55,29 @@ type LinodeMachineTemplate struct {
4155
metav1.ObjectMeta `json:"metadata,omitempty"`
4256

4357
Spec LinodeMachineTemplateSpec `json:"spec,omitempty"`
58+
59+
Status LinodeMachineTemplateStatus `json:"status,omitempty"`
60+
}
61+
62+
func (lmt *LinodeMachineTemplate) GetConditions() []metav1.Condition {
63+
for i := range lmt.Status.Conditions {
64+
if lmt.Status.Conditions[i].Reason == "" {
65+
lmt.Status.Conditions[i].Reason = DefaultConditionReason
66+
}
67+
}
68+
return lmt.Status.Conditions
69+
}
70+
71+
func (lmt *LinodeMachineTemplate) SetConditions(conditions []metav1.Condition) {
72+
lmt.Status.Conditions = conditions
73+
}
74+
75+
func (lmt *LinodeMachineTemplate) GetV1Beta2Conditions() []metav1.Condition {
76+
return lmt.GetConditions()
77+
}
78+
79+
func (lmt *LinodeMachineTemplate) SetV1Beta2Conditions(conditions []metav1.Condition) {
80+
lmt.SetConditions(conditions)
4481
}
4582

4683
// +kubebuilder:object:root=true

api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/scope/machine_template.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2025 Akamai Technologies, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package scope
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
24+
"sigs.k8s.io/cluster-api/util/patch"
25+
26+
infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2"
27+
"github.com/linode/cluster-api-provider-linode/clients"
28+
)
29+
30+
// MachineTemplateScope defines the basic context for an actuator to operate upon.
31+
type MachineTemplateScope struct {
32+
PatchHelper *patch.Helper
33+
LinodeMachineTemplate *infrav1alpha2.LinodeMachineTemplate
34+
}
35+
36+
// MachineTemplateScopeParams defines the input parameters used to create a new MachineTemplateScope.
37+
type MachineTemplateScopeParams struct {
38+
Client clients.K8sClient
39+
LinodeMachineTemplate *infrav1alpha2.LinodeMachineTemplate
40+
}
41+
42+
// validateMachineTemplateScope validates the parameters for creating a MachineTemplateScope.
43+
func validateMachineTemplateScope(params MachineTemplateScopeParams) error {
44+
if params.LinodeMachineTemplate == nil {
45+
return errors.New("LinodeMachineTemplate is required when creating a MachineTemplateScope")
46+
}
47+
48+
return nil
49+
}
50+
51+
// PatchObject persists the machine template configuration and status.
52+
func (s *MachineTemplateScope) PatchObject(ctx context.Context) error {
53+
return s.PatchHelper.Patch(ctx, s.LinodeMachineTemplate)
54+
}
55+
56+
// Close closes the current scope persisting the machine template configuration and status.
57+
func (s *MachineTemplateScope) Close(ctx context.Context) error {
58+
return s.PatchObject(ctx)
59+
}
60+
61+
// NewMachineTemplateScope creates a new Scope from the supplied parameters.
62+
// This is meant to be called for each reconcile iteration.
63+
func NewMachineTemplateScope(ctx context.Context, params MachineTemplateScopeParams) (*MachineTemplateScope, error) {
64+
if err := validateMachineTemplateScope(params); err != nil {
65+
return nil, err
66+
}
67+
68+
helper, err := patch.NewHelper(params.LinodeMachineTemplate, params.Client)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to init patch helper: %w", err)
71+
}
72+
73+
return &MachineTemplateScope{
74+
LinodeMachineTemplate: params.LinodeMachineTemplate,
75+
PatchHelper: helper,
76+
}, nil
77+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
Copyright 2025 Akamai Technologies, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package scope
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
"go.uber.org/mock/gomock"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
28+
infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2"
29+
"github.com/linode/cluster-api-provider-linode/mock"
30+
)
31+
32+
func TestValidateMachineTemplateScope(t *testing.T) {
33+
t.Parallel()
34+
tests := []struct {
35+
name string
36+
LinodeMachineTemplate *infrav1alpha2.LinodeMachineTemplate
37+
expErr string
38+
}{
39+
{
40+
name: "Success - valid LinodeMachineTemplate",
41+
LinodeMachineTemplate: &infrav1alpha2.LinodeMachineTemplate{
42+
ObjectMeta: metav1.ObjectMeta{
43+
Name: "test-lmt",
44+
},
45+
},
46+
expErr: "",
47+
},
48+
{
49+
name: "Failure - nil LinodeMachineTemplate",
50+
LinodeMachineTemplate: nil,
51+
expErr: "LinodeMachineTemplate is required when creating a MachineTemplateScope",
52+
},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
t.Parallel()
58+
59+
err := validateMachineTemplateScope(MachineTemplateScopeParams{
60+
LinodeMachineTemplate: tt.LinodeMachineTemplate,
61+
})
62+
63+
if tt.expErr != "" {
64+
assert.ErrorContains(t, err, tt.expErr)
65+
} else {
66+
assert.NoError(t, err)
67+
}
68+
})
69+
}
70+
}
71+
72+
func TestNewMachineTemplateScope(t *testing.T) {
73+
t.Parallel()
74+
tests := []struct {
75+
name string
76+
LinodeMachineTemplate *infrav1alpha2.LinodeMachineTemplate
77+
expErr string
78+
expects func(mock *mock.MockK8sClient)
79+
}{
80+
{
81+
name: "Success - able to create a new MachineTemplateScope",
82+
LinodeMachineTemplate: &infrav1alpha2.LinodeMachineTemplate{
83+
ObjectMeta: metav1.ObjectMeta{
84+
Name: "test-lmt",
85+
},
86+
},
87+
expects: func(mock *mock.MockK8sClient) {
88+
scheme := runtime.NewScheme()
89+
infrav1alpha2.AddToScheme(scheme)
90+
mock.EXPECT().Scheme().Return(scheme)
91+
},
92+
},
93+
{
94+
name: "Failure - nil LinodeMachineTemplate",
95+
LinodeMachineTemplate: nil,
96+
expErr: "LinodeMachineTemplate is required",
97+
},
98+
}
99+
for _, tt := range tests {
100+
testcase := tt
101+
t.Run(testcase.name, func(t *testing.T) {
102+
t.Parallel()
103+
104+
ctrl := gomock.NewController(t)
105+
defer ctrl.Finish()
106+
107+
mockK8sClient := mock.NewMockK8sClient(ctrl)
108+
109+
if tt.expects != nil {
110+
tt.expects(mockK8sClient)
111+
}
112+
113+
lmtScope, err := NewMachineTemplateScope(
114+
t.Context(),
115+
MachineTemplateScopeParams{
116+
Client: mockK8sClient,
117+
LinodeMachineTemplate: testcase.LinodeMachineTemplate,
118+
},
119+
)
120+
121+
if tt.expErr != "" {
122+
require.ErrorContains(t, err, tt.expErr)
123+
} else {
124+
require.NoError(t, err)
125+
require.NotNil(t, lmtScope)
126+
require.Equal(t, lmtScope.LinodeMachineTemplate, testcase.LinodeMachineTemplate)
127+
}
128+
})
129+
}
130+
}

cmd/main.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type flagVars struct {
8888
linodeVPCConcurrency int
8989
linodePlacementGroupConcurrency int
9090
linodeFirewallConcurrency int
91+
linodeMachineTemplateConcurrency int
9192
}
9293

9394
func init() {
@@ -152,7 +153,7 @@ func parseFlags() (flags flagVars, opts zap.Options) {
152153
flag.IntVar(&flags.linodeVPCConcurrency, "linodevpc-concurrency", concurrencyDefault, "Number of LinodeVPCs to process simultaneously")
153154
flag.IntVar(&flags.linodePlacementGroupConcurrency, "linodeplacementgroup-concurrency", concurrencyDefault, "Number of Linode Placement Groups to process simultaneously")
154155
flag.IntVar(&flags.linodeFirewallConcurrency, "linodefirewall-concurrency", concurrencyDefault, "Number of Linode Firewall to process simultaneously")
155-
156+
flag.IntVar(&flags.linodeMachineTemplateConcurrency, "linodemachinetemplate-concurrency", concurrencyDefault, "Number of LinodeMachineTemplates to process simultaneously")
156157
opts = zap.Options{Development: true}
157158
opts.BindFlags(flag.CommandLine)
158159
flag.Parse()
@@ -347,6 +348,15 @@ func setupControllers(mgr manager.Manager, flags flagVars, linodeClientConfig, d
347348
setupLog.Error(err, "unable to create controller", "controller", "LinodeFirewall")
348349
os.Exit(1)
349350
}
351+
352+
// LinodeMachineTemplate Controller
353+
if err := (&controller.LinodeMachineTemplateReconciler{
354+
Client: mgr.GetClient(),
355+
Logger: ctrl.Log.WithName("LinodeMachineTemplateReconciler"),
356+
}).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: flags.linodeMachineTemplateConcurrency}); err != nil {
357+
setupLog.Error(err, "unable to create controller", "controller", "LinodeMachineTemplate")
358+
os.Exit(1)
359+
}
350360
}
351361

352362
// setupWebhooks initializes webhooks for the specified resources in the manager.

0 commit comments

Comments
 (0)