Skip to content

Commit fe0fc17

Browse files
committed
feat: support creating config patches in the infrastructure providers
Patches can be created for a single machine in the machine provision flow: the provider can call `CreateConfigPatch` method at any point. This will create a `ConfigPatchRequest` resource which will be turned into a `ConfigPatch` after the `MachineRequestStatus` UUID gets populated. Fixes: #728 Signed-off-by: Artem Chernyshev <[email protected]>
1 parent 3e8bc8d commit fe0fc17

File tree

11 files changed

+336
-2
lines changed

11 files changed

+336
-2
lines changed

client/pkg/infra/controllers/provision.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ func (ctrl *ProvisionController[T]) Settings() controller.QSettings {
7676
Kind: controller.InputQMapped,
7777
ID: optional.Some(siderolink.ConfigID),
7878
},
79+
{
80+
Namespace: resources.InfraProviderNamespace,
81+
Type: infra.ConfigPatchRequestType,
82+
Kind: controller.InputQMappedDestroyReady,
83+
},
7984
{
8085
Namespace: t.ResourceDefinition().DefaultNamespace,
8186
Type: t.ResourceDefinition().Type,
@@ -91,6 +96,10 @@ func (ctrl *ProvisionController[T]) Settings() controller.QSettings {
9196
Kind: controller.OutputShared,
9297
Type: t.ResourceDefinition().Type,
9398
},
99+
{
100+
Kind: controller.OutputShared,
101+
Type: infra.ConfigPatchRequestType,
102+
},
94103
},
95104
Concurrency: optional.Some(ctrl.concurrency),
96105
}
@@ -214,6 +223,7 @@ func (ctrl *ProvisionController[T]) reconcileRunning(ctx context.Context, r cont
214223
st,
215224
connectionParams,
216225
ctrl.imageFactory,
226+
r,
217227
))
218228

219229
st.Metadata().Annotations().Set(currentStepAnnotation, step.Name())
@@ -259,6 +269,41 @@ func (ctrl *ProvisionController[T]) reconcileRunning(ctx context.Context, r cont
259269
return nil
260270
}
261271

272+
func (ctrl *ProvisionController[T]) removePatches(ctx context.Context, r controller.QRuntime, requestID string) (bool, error) {
273+
destroyReady := true
274+
275+
patches, err := safe.ReaderListAll[*infra.ConfigPatchRequest](ctx, r, state.WithLabelQuery(
276+
resource.LabelEqual(omni.LabelInfraProviderID, ctrl.providerID),
277+
resource.LabelEqual(omni.LabelMachineRequest, requestID),
278+
))
279+
if err != nil {
280+
return false, err
281+
}
282+
283+
for request := range patches.All() {
284+
ready, err := r.Teardown(ctx, request.Metadata())
285+
if err != nil {
286+
if state.IsNotFoundError(err) {
287+
continue
288+
}
289+
290+
return false, err
291+
}
292+
293+
if !ready {
294+
destroyReady = false
295+
296+
continue
297+
}
298+
299+
if err = r.Destroy(ctx, request.Metadata()); err != nil && !state.IsNotFoundError(err) {
300+
return false, err
301+
}
302+
}
303+
304+
return destroyReady, nil
305+
}
306+
262307
func (ctrl *ProvisionController[T]) initializeStatus(ctx context.Context, r controller.QRuntime, logger *zap.Logger, machineRequest *infra.MachineRequest) (*infra.MachineRequestStatus, error) {
263308
mrs, err := safe.ReaderGetByID[*infra.MachineRequestStatus](ctx, r, machineRequest.Metadata().ID())
264309
if err != nil && !state.IsNotFoundError(err) {
@@ -309,6 +354,18 @@ func (ctrl *ProvisionController[T]) reconcileTearingDown(ctx context.Context, r
309354
return err
310355
}
311356

357+
{
358+
var ready bool
359+
360+
if ready, err = ctrl.removePatches(ctx, r, machineRequest.Metadata().ID()); err != nil {
361+
return err
362+
}
363+
364+
if !ready {
365+
return nil
366+
}
367+
}
368+
312369
resources := []resource.Metadata{
313370
resource.NewMetadata(t.ResourceDefinition().DefaultNamespace, t.ResourceDefinition().Type, machineRequest.Metadata().ID(), resource.VersionUndefined),
314371
*infra.NewMachineRequestStatus(machineRequest.Metadata().ID()).Metadata(),

client/pkg/infra/infra_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ func (p *provisioner) ProvisionSteps() []provision.Step[*TestResource] {
186186

187187
return nil
188188
}),
189+
provision.NewStep("patches", func(ctx context.Context, _ *zap.Logger, pctx provision.Context[*TestResource]) error {
190+
return pctx.CreateConfigPatch(ctx, pctx.GetRequestID(), []byte("machine: {}"))
191+
}),
189192
provision.NewStep("schematic", genSchematic),
190193
provision.NewStep("validate", validateConnectionParams),
191194
provision.NewStep("provision", func(ctx context.Context, _ *zap.Logger, pctx provision.Context[*TestResource]) error {
@@ -269,6 +272,8 @@ func TestInfra(t *testing.T) {
269272
machineRequest.Metadata().Labels().Set(omni.LabelInfraProviderID, providerID)
270273
machineRequest.Metadata().Labels().Set(customLabel, customValue)
271274

275+
patchID := machineRequest.Metadata().ID()
276+
272277
require.NoError(t, state.Create(ctx, machineRequest))
273278

274279
connectionParams := siderolink.NewConnectionParams(resources.DefaultNamespace, siderolink.ConfigID)
@@ -299,6 +304,13 @@ func TestInfra(t *testing.T) {
299304
assert.Equal(specs.MachineRequestStatusSpec_PROVISIONED, machineRequestStatus.TypedSpec().Value.Stage)
300305
})
301306

307+
rtestutils.AssertResources(ctx, t, state, []string{patchID}, func(r *infrares.ConfigPatchRequest, assert *assert.Assertions) {
308+
data, err := r.TypedSpec().Value.GetUncompressedData()
309+
310+
assert.NoError(err)
311+
assert.EqualValues([]byte("machine: {}"), data.Data())
312+
})
313+
302314
rtestutils.AssertResources(ctx, t, state, []string{machineRequest.Metadata().ID()}, func(testResource *TestResource, assert *assert.Assertions) {
303315
assert.True(testResource.TypedSpec().Value.Connected)
304316
})
@@ -311,6 +323,8 @@ func TestInfra(t *testing.T) {
311323
rtestutils.AssertNoResource[*TestResource](ctx, t, state, machineRequest.Metadata().ID())
312324

313325
require.Nil(t, p.getMachine(machineRequest.Metadata().ID()))
326+
327+
rtestutils.AssertNoResource[*infrares.ConfigPatchRequest](ctx, t, state, patchID)
314328
}
315329

316330
func setupInfra(ctx context.Context, t *testing.T, p *provisioner) state.State {

client/pkg/infra/provision/context.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ package provision
66

77
import (
88
"context"
9+
"errors"
910
"slices"
1011
"strings"
1112

13+
"github.com/cosi-project/runtime/pkg/controller"
1214
"github.com/cosi-project/runtime/pkg/resource"
15+
"github.com/cosi-project/runtime/pkg/safe"
1316
"github.com/siderolabs/gen/xslices"
1417
"github.com/siderolabs/image-factory/pkg/schematic"
1518
"go.uber.org/zap"
1619
"gopkg.in/yaml.v3"
1720

1821
"github.com/siderolabs/omni/client/api/omni/specs"
22+
"github.com/siderolabs/omni/client/pkg/omni/resources"
1923
"github.com/siderolabs/omni/client/pkg/omni/resources/infra"
2024
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
2125
)
@@ -90,13 +94,15 @@ func NewContext[T resource.Resource](
9094
state T,
9195
connectionParams ConnectionParams,
9296
imageFactory FactoryClient,
97+
runtime controller.QRuntime,
9398
) Context[T] {
9499
return Context[T]{
95100
machineRequest: machineRequest,
96101
MachineRequestStatus: machineRequestStatus,
97102
State: state,
98103
ConnectionParams: connectionParams,
99104
imageFactory: imageFactory,
105+
runtime: runtime,
100106
}
101107
}
102108

@@ -105,6 +111,7 @@ type Context[T resource.Resource] struct {
105111
machineRequest *infra.MachineRequest
106112
imageFactory FactoryClient
107113
MachineRequestStatus *infra.MachineRequestStatus
114+
runtime controller.QRuntime
108115
State T
109116
ConnectionParams ConnectionParams
110117
}
@@ -134,6 +141,23 @@ func (context *Context[T]) UnmarshalProviderData(dest any) error {
134141
return yaml.Unmarshal([]byte(context.machineRequest.TypedSpec().Value.ProviderData), dest)
135142
}
136143

144+
// CreateConfigPatch for the provisioned machine.
145+
func (context *Context[T]) CreateConfigPatch(ctx context.Context, name string, data []byte) error {
146+
r := infra.NewConfigPatchRequest(resources.InfraProviderNamespace, name)
147+
148+
providerID, ok := context.machineRequest.Metadata().Labels().Get(omni.LabelInfraProviderID)
149+
if !ok {
150+
return errors.New("infra provider id is not set on the machine request")
151+
}
152+
153+
return safe.WriterModify(ctx, context.runtime, r, func(r *infra.ConfigPatchRequest) error {
154+
r.Metadata().Labels().Set(omni.LabelInfraProviderID, providerID)
155+
r.Metadata().Labels().Set(omni.LabelMachineRequest, context.GetRequestID())
156+
157+
return r.TypedSpec().Value.SetUncompressedData(data)
158+
})
159+
}
160+
137161
// GenerateSchematicID generate the final schematic out of the machine request.
138162
// This method also calls the image factory and uploads the schematic there.
139163
func (context *Context[T]) GenerateSchematicID(ctx context.Context, logger *zap.Logger, opts ...SchematicOption) (string, error) {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package infra
6+
7+
import (
8+
"github.com/cosi-project/runtime/pkg/resource"
9+
"github.com/cosi-project/runtime/pkg/resource/meta"
10+
"github.com/cosi-project/runtime/pkg/resource/protobuf"
11+
"github.com/cosi-project/runtime/pkg/resource/typed"
12+
13+
"github.com/siderolabs/omni/client/api/omni/specs"
14+
"github.com/siderolabs/omni/client/pkg/omni/resources"
15+
)
16+
17+
// NewConfigPatchRequest creates new ConfigPatchRequest resource.
18+
func NewConfigPatchRequest(ns string, id resource.ID) *ConfigPatchRequest {
19+
return typed.NewResource[ConfigPatchRequestSpec, ConfigPatchRequestExtension](
20+
resource.NewMetadata(ns, ConfigPatchRequestType, id, resource.VersionUndefined),
21+
protobuf.NewResourceSpec(&specs.ConfigPatchSpec{}),
22+
)
23+
}
24+
25+
const (
26+
// ConfigPatchRequestType is the type of the ConfigPatch resource.
27+
// tsgen:ConfigPatchRequestType
28+
ConfigPatchRequestType = resource.Type("ConfigPatchRequests.omni.sidero.dev")
29+
)
30+
31+
// ConfigPatchRequest requests a config patch to be created for the machine.
32+
// The controller should copy this resource contents to the target config patch, if the patch is valid.
33+
type ConfigPatchRequest = typed.Resource[ConfigPatchRequestSpec, ConfigPatchRequestExtension]
34+
35+
// ConfigPatchRequestSpec wraps specs.ConfigPatchRequestSpec.
36+
type ConfigPatchRequestSpec = protobuf.ResourceSpec[specs.ConfigPatchSpec, *specs.ConfigPatchSpec]
37+
38+
// ConfigPatchRequestExtension provides auxiliary methods for ConfigPatch resource.
39+
type ConfigPatchRequestExtension struct{}
40+
41+
// ResourceDefinition implements [typed.Extension] interface.
42+
func (ConfigPatchRequestExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
43+
return meta.ResourceDefinitionSpec{
44+
Type: ConfigPatchRequestType,
45+
Aliases: []resource.Type{},
46+
DefaultNamespace: resources.InfraProviderNamespace,
47+
PrintColumns: []meta.PrintColumn{},
48+
Sensitivity: meta.Sensitive,
49+
}
50+
}

client/pkg/omni/resources/infra/infra.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ func init() {
1111
registry.MustRegisterResource(MachineRequestType, &MachineRequest{})
1212
registry.MustRegisterResource(MachineRequestStatusType, &MachineRequestStatus{})
1313
registry.MustRegisterResource(InfraProviderStatusType, &ProviderStatus{})
14+
registry.MustRegisterResource(ConfigPatchRequestType, &ConfigPatchRequest{})
1415
}

cmd/integration-test/pkg/tests/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,7 @@ func AssertResourceAuthz(rootCtx context.Context, rootCli *client.Client, client
10421042
delete(untestedResourceTypes, infra.MachineRequestType)
10431043
delete(untestedResourceTypes, infra.MachineRequestStatusType)
10441044
delete(untestedResourceTypes, infra.InfraProviderStatusType)
1045+
delete(untestedResourceTypes, infra.ConfigPatchRequestType)
10451046

10461047
for _, tc := range testCases {
10471048
for _, testVerb := range allVerbs {

cmd/integration-test/pkg/tests/stats.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func AssertStatsLimits(testCtx context.Context) TestFunc {
3535
{
3636
name: "resource CRUD",
3737
query: `sum(omni_resource_operations_total{operation=~"create|update", type!="MachineStatusLinks.omni.sidero.dev"})`,
38-
check: func(assert *assert.Assertions, value float64) { assert.Less(value, float64(10000)) },
38+
check: func(assert *assert.Assertions, value float64) { assert.Less(value, float64(11000)) },
3939
},
4040
{
4141
name: "queue length",
@@ -45,7 +45,7 @@ func AssertStatsLimits(testCtx context.Context) TestFunc {
4545
{
4646
name: "controller wakeups",
4747
query: `sum(omni_runtime_controller_wakeups{controller!="MachineStatusLinkController"})`,
48-
check: func(assert *assert.Assertions, value float64) { assert.Less(value, float64(10000)) },
48+
check: func(assert *assert.Assertions, value float64) { assert.Less(value, float64(11000)) },
4949
},
5050
} {
5151
t.Run(tt.name, func(t *testing.T) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2024 Sidero Labs, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the LICENSE file.
5+
6+
package omni
7+
8+
import (
9+
"context"
10+
"errors"
11+
12+
"github.com/cosi-project/runtime/pkg/controller"
13+
"github.com/cosi-project/runtime/pkg/controller/generic/qtransform"
14+
"github.com/cosi-project/runtime/pkg/resource"
15+
"github.com/cosi-project/runtime/pkg/safe"
16+
"github.com/cosi-project/runtime/pkg/state"
17+
"github.com/siderolabs/gen/xerrors"
18+
"go.uber.org/zap"
19+
20+
"github.com/siderolabs/omni/client/pkg/omni/resources"
21+
"github.com/siderolabs/omni/client/pkg/omni/resources/infra"
22+
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
23+
"github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/helpers"
24+
)
25+
26+
// InfraProviderConfigPatchController manages endpoints for each Cluster.
27+
type InfraProviderConfigPatchController = qtransform.QController[*infra.ConfigPatchRequest, *omni.ConfigPatch]
28+
29+
// NewInfraProviderConfigPatchController initializes ConfigPatchRequestController.
30+
func NewInfraProviderConfigPatchController() *InfraProviderConfigPatchController {
31+
return qtransform.NewQController(
32+
qtransform.Settings[*infra.ConfigPatchRequest, *omni.ConfigPatch]{
33+
Name: "ConfigPatchRequestController",
34+
MapMetadataFunc: func(request *infra.ConfigPatchRequest) *omni.ConfigPatch {
35+
return omni.NewConfigPatch(resources.DefaultNamespace, request.Metadata().ID())
36+
},
37+
UnmapMetadataFunc: func(configPatch *omni.ConfigPatch) *infra.ConfigPatchRequest {
38+
return infra.NewConfigPatchRequest(resources.DefaultNamespace, configPatch.Metadata().ID())
39+
},
40+
TransformFunc: func(ctx context.Context, r controller.Reader, _ *zap.Logger, request *infra.ConfigPatchRequest, patch *omni.ConfigPatch) error {
41+
machineRequestID, ok := request.Metadata().Labels().Get(omni.LabelMachineRequest)
42+
if !ok {
43+
return xerrors.NewTaggedf[qtransform.DestroyOutputTag]("missing machine request label on the patch request")
44+
}
45+
46+
machineRequestStatus, err := safe.ReaderGetByID[*infra.MachineRequestStatus](ctx, r, machineRequestID)
47+
if err != nil {
48+
if state.IsNotFoundError(err) {
49+
return xerrors.NewTaggedf[qtransform.DestroyOutputTag]("machine request status with id %q doesn't exist", machineRequestID)
50+
}
51+
52+
return err
53+
}
54+
55+
if machineRequestStatus.TypedSpec().Value.Id == "" {
56+
return errors.New("failed to create config patch from the request: machine request status doesn't have machine UUID")
57+
}
58+
59+
patch.TypedSpec().Value = request.TypedSpec().Value
60+
61+
helpers.CopyAllLabels(request, patch)
62+
63+
patch.Metadata().Labels().Set(omni.LabelSystemPatch, "")
64+
patch.Metadata().Labels().Set(omni.LabelMachine, machineRequestStatus.TypedSpec().Value.Id)
65+
66+
return nil
67+
},
68+
},
69+
qtransform.WithExtraMappedInput(
70+
func(ctx context.Context, _ *zap.Logger, r controller.QRuntime, machineRequestStatus *infra.MachineRequestStatus) ([]resource.Pointer, error) {
71+
patchRequests, err := safe.ReaderListAll[*infra.ConfigPatchRequest](ctx, r, state.WithLabelQuery(
72+
resource.LabelEqual(omni.LabelMachineRequest, machineRequestStatus.Metadata().ID())),
73+
)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return safe.ToSlice(patchRequests, func(r *infra.ConfigPatchRequest) resource.Pointer { return r.Metadata() }), nil
79+
},
80+
),
81+
qtransform.WithOutputKind(controller.OutputShared),
82+
)
83+
}

0 commit comments

Comments
 (0)