Skip to content

Commit b737818

Browse files
committed
Add remotePullPolicy to reduce remote module download costs
Signed-off-by: Fatih Türken <turkenf@gmail.com>
1 parent 24d6b81 commit b737818

10 files changed

Lines changed: 786 additions & 32 deletions

File tree

apis/cluster/v1beta1/workspace_types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ const (
9292
ModuleSourceInline ModuleSource = "Inline"
9393
)
9494

95+
// RemotePullPolicy determines when to pull remote module sources.
96+
// +kubebuilder:validation:Enum=Always;IfNotPresent
97+
type RemotePullPolicy string
98+
99+
// Remote pull policies.
100+
const (
101+
// RemotePullPolicyAlways pulls remote source on every reconciliation (default)
102+
RemotePullPolicyAlways RemotePullPolicy = "Always"
103+
104+
// RemotePullPolicyIfNotPresent pulls remote source only if not already present
105+
RemotePullPolicyIfNotPresent RemotePullPolicy = "IfNotPresent"
106+
)
107+
95108
// WorkspaceParameters are the configurable fields of a Workspace.
96109
type WorkspaceParameters struct {
97110
// The root module of this workspace; i.e. the module containing its main.tf
@@ -145,12 +158,21 @@ type WorkspaceParameters struct {
145158
// Boolean value to indicate CLI logging of tofu execution is enabled or not
146159
// +optional
147160
EnableTofuCLILogging bool `json:"enableTofuCLILogging,omitempty"`
161+
162+
// RemotePullPolicy determines when to download remote module sources.
163+
// +optional
164+
// +kubebuilder:default=Always
165+
RemotePullPolicy *RemotePullPolicy `json:"remotePullPolicy,omitempty"`
148166
}
149167

150168
// WorkspaceObservation are the observable fields of a Workspace.
151169
type WorkspaceObservation struct {
152170
Checksum string `json:"checksum,omitempty"`
153171
Outputs map[string]extensionsV1.JSON `json:"outputs,omitempty"`
172+
173+
// RemoteSource is the remote module URL that was last retrieved
174+
// +optional
175+
RemoteSource string `json:"remoteSource,omitempty"`
154176
}
155177

156178
// A WorkspaceSpec defines the desired state of a Workspace.

apis/cluster/v1beta1/zz_generated.deepcopy.go

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

apis/namespaced/v1beta1/workspace_types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ const (
9191
ModuleSourceInline ModuleSource = "Inline"
9292
)
9393

94+
// RemotePullPolicy determines when to pull remote module sources.
95+
// +kubebuilder:validation:Enum=Always;IfNotPresent
96+
type RemotePullPolicy string
97+
98+
// Remote pull policies.
99+
const (
100+
// RemotePullPolicyAlways pulls remote source on every reconciliation (default)
101+
RemotePullPolicyAlways RemotePullPolicy = "Always"
102+
103+
// RemotePullPolicyIfNotPresent pulls remote source only if not already present
104+
RemotePullPolicyIfNotPresent RemotePullPolicy = "IfNotPresent"
105+
)
106+
94107
// WorkspaceParameters are the configurable fields of a Workspace.
95108
type WorkspaceParameters struct {
96109
// The root module of this workspace; i.e. the module containing its main.tf
@@ -144,12 +157,21 @@ type WorkspaceParameters struct {
144157
// Boolean value to indicate CLI logging of tofu execution is enabled or not
145158
// +optional
146159
EnableTofuCLILogging bool `json:"enableTofuCLILogging,omitempty"`
160+
161+
// RemotePullPolicy determines when to download remote module sources.
162+
// +optional
163+
// +kubebuilder:default=Always
164+
RemotePullPolicy *RemotePullPolicy `json:"remotePullPolicy,omitempty"`
147165
}
148166

149167
// WorkspaceObservation are the observable fields of a Workspace.
150168
type WorkspaceObservation struct {
151169
Checksum string `json:"checksum,omitempty"`
152170
Outputs map[string]extensionsV1.JSON `json:"outputs,omitempty"`
171+
172+
// RemoteSource is the remote module URL that was last retrieved
173+
// +optional
174+
RemoteSource string `json:"remoteSource,omitempty"`
153175
}
154176

155177
// A WorkspaceSpec defines the desired state of a Workspace.

apis/namespaced/v1beta1/zz_generated.deepcopy.go

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

internal/controller/cluster/workspace/workspace.go

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,67 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E
225225
}
226226
}
227227

228+
// Calculate the final tofu working directory (including entrypoint if specified)
229+
// This is where tofu will actually run and create .terraform directory
230+
tofuWorkDir := dir
231+
if len(cr.Spec.ForProvider.Entrypoint) > 0 {
232+
entrypoint := strings.ReplaceAll(cr.Spec.ForProvider.Entrypoint, "../", "")
233+
tofuWorkDir = filepath.Join(dir, entrypoint)
234+
}
235+
228236
switch cr.Spec.ForProvider.Source {
229237
case v1beta1.ModuleSourceRemote:
230-
gc := getter.Client{
231-
Src: cr.Spec.ForProvider.Module,
232-
Dst: dir,
233-
Pwd: dir,
238+
shouldPull := false
239+
240+
// Determine if we should pull the remote source
241+
switch {
242+
case cr.Spec.ForProvider.RemotePullPolicy == nil ||
243+
*cr.Spec.ForProvider.RemotePullPolicy == v1beta1.RemotePullPolicyAlways:
244+
// Always pull (default behavior)
245+
shouldPull = true
246+
l.Debug("Remote module pull policy: Always")
247+
248+
case *cr.Spec.ForProvider.RemotePullPolicy == v1beta1.RemotePullPolicyIfNotPresent:
249+
// Check if .terraform.lock.hcl exists (indicates successful init)
250+
// This file is created at the END of tofu init, making it the
251+
// most reliable indicator that module initialization completed
252+
lockFile := filepath.Join(tofuWorkDir, ".terraform.lock.hcl")
253+
lockFileValid, err := validateTofuLockFile(c.fs, lockFile)
254+
if err != nil {
255+
return nil, errors.Wrap(err, "failed to validate tofu lock file")
256+
}
234257

235-
Mode: getter.ClientModeDir,
258+
if lockFileValid {
259+
// Module already initialized - check if spec changed
260+
if cr.Spec.ForProvider.Module != cr.Status.AtProvider.RemoteSource {
261+
l.Debug("Remote module URL changed", "old", cr.Status.AtProvider.RemoteSource, "new", cr.Spec.ForProvider.Module)
262+
shouldPull = true
263+
} else {
264+
l.Debug("Remote module already initialized, skipping download", "lockFile", lockFile)
265+
shouldPull = false
266+
}
267+
} else {
268+
l.Debug("Tofu not initialized, downloading module", "lockFile", lockFile)
269+
shouldPull = true
270+
}
236271
}
237-
err := gc.Get()
238-
if err != nil {
239-
return nil, errors.Wrap(err, errRemoteModule)
272+
273+
// Pull remote source if needed
274+
if shouldPull {
275+
gc := getter.Client{
276+
Src: cr.Spec.ForProvider.Module,
277+
Dst: dir,
278+
Pwd: dir,
279+
Mode: getter.ClientModeDir,
280+
}
281+
err := gc.Get()
282+
if err != nil {
283+
return nil, errors.Wrap(err, errRemoteModule)
284+
}
285+
286+
// Update status with downloaded module URL
287+
cr.Status.AtProvider.RemoteSource = cr.Spec.ForProvider.Module
288+
l.Debug("Remote module downloaded", "url", cr.Spec.ForProvider.Module)
240289
}
241290

242291
case v1beta1.ModuleSourceInline:
@@ -389,13 +438,15 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
389438
if err != nil {
390439
return managed.ExternalObservation{}, errors.Wrap(err, errOutputs)
391440
}
392-
cr.Status.AtProvider = generateWorkspaceObservation(op)
393-
441+
// Generate checksum first
394442
checksum, err := c.tofu.GenerateChecksum(ctx)
395443
if err != nil {
396444
return managed.ExternalObservation{}, errors.Wrap(err, errChecksum)
397445
}
398-
cr.Status.AtProvider.Checksum = checksum
446+
447+
// Preserve remoteSource from previous status (set in Connect)
448+
// Generate observation with all persistent fields
449+
cr.Status.AtProvider = generateWorkspaceObservation(op, checksum, cr.Status.AtProvider.RemoteSource)
399450

400451
if !differs {
401452
// TODO(negz): Allow Workspaces to optionally derive their readiness from an
@@ -438,7 +489,16 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
438489
if err != nil {
439490
return managed.ExternalUpdate{}, errors.Wrap(err, errOutputs)
440491
}
441-
cr.Status.AtProvider = generateWorkspaceObservation(op)
492+
493+
// Generate checksum after apply
494+
checksum, err := c.tofu.GenerateChecksum(ctx)
495+
if err != nil {
496+
return managed.ExternalUpdate{}, errors.Wrap(err, errChecksum)
497+
}
498+
499+
// Preserve remoteSource and update observation with checksum
500+
cr.Status.AtProvider = generateWorkspaceObservation(op, checksum, cr.Status.AtProvider.RemoteSource)
501+
442502
// TODO(negz): Allow Workspaces to optionally derive their readiness from an
443503
// output - similar to the logic XRs use to derive readiness from a field of
444504
// a composed resource.
@@ -528,11 +588,38 @@ func op2cd(o []opentofu.Output) managed.ConnectionDetails {
528588
return cd
529589
}
530590

591+
// validateTofuLockFile checks if .terraform.lock.hcl exists and is valid.
592+
// This file is created at the END of successful tofu init, making it the
593+
// most reliable indicator that module initialization completed successfully.
594+
func validateTofuLockFile(fs afero.Afero, lockFilePath string) (bool, error) {
595+
data, err := fs.ReadFile(lockFilePath)
596+
if err != nil {
597+
if os.IsNotExist(err) {
598+
return false, nil // File doesn't exist, not an error
599+
}
600+
return false, err // Read error
601+
}
602+
603+
if len(data) == 0 {
604+
return false, nil // Empty file (interrupted write)
605+
}
606+
607+
// Basic validation: lock file should contain "provider" declarations
608+
content := string(data)
609+
if !strings.Contains(content, "provider") {
610+
return false, nil // Doesn't look like a valid lock file
611+
}
612+
613+
return true, nil
614+
}
615+
531616
// generateWorkspaceObservation is used to produce v1beta1.WorkspaceObservation from
532617
// workspace_type.Workspace.
533-
func generateWorkspaceObservation(op []opentofu.Output) v1beta1.WorkspaceObservation {
618+
func generateWorkspaceObservation(op []opentofu.Output, checksum, remoteSource string) v1beta1.WorkspaceObservation {
534619
wo := v1beta1.WorkspaceObservation{
535-
Outputs: make(map[string]extensionsV1.JSON, len(op)),
620+
Outputs: make(map[string]extensionsV1.JSON, len(op)),
621+
Checksum: checksum,
622+
RemoteSource: remoteSource,
536623
}
537624
for _, o := range op {
538625
if !o.Sensitive {

0 commit comments

Comments
 (0)