Skip to content
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 .github/workflows/update-terraform-provider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.22'
go-version: '1.25'

- name: Install golangci-lint
uses: golangci/golangci-lint-action@v8
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ $ kubectl create namespace crossplane-system --dry-run=client -o yaml | kubectl
| `organization_id` | The [organization ID](https://console.scaleway.com/organization/settings) that will be used as default value for organization-scoped resources. |
| `region` | The [region](https://developers.scaleway.com/en/quickstart/#region-and-zone) that will be used as default value for all resources. (`fr-par` if none specified) |
| `zone` | The [zone](https://developers.scaleway.com/en/quickstart/#region-and-zone) that will be used as default value for all resources. (`fr-par-1` if none specified) |
| `api_url` | The URL of the API |

### Create a ProviderConfig

Expand Down Expand Up @@ -154,6 +155,23 @@ The `spec.secretRef` describes the parameters of the secret to use.
* `name` is the name of the Kubernetes `secret` object.
* `key` is the `Data` field from `kubectl describe secret`.

### SCW config support

This provider can read the standard SCW config file (`~/.config/scw/config.yaml`) and environment variables. Precedence is:

1. ProviderConfig credentials
2. Environment variables (`SCW_*`)
3. SCW config file

You can control behavior in `ProviderConfig.spec.scw`:

```yaml
spec:
scw:
useScwConfig: true
# path: /home/me/.config/scw/config.yaml
# profile: myProfile
```
### Create a managed resource

1. Create a managed resource to see if the provider is properly functioning.
Expand Down
1 change: 0 additions & 1 deletion apis/generate.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//go:build generate
// +build generate

/*
Copyright 2021 Upbound Inc.
Expand Down
18 changes: 18 additions & 0 deletions apis/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import (
// A ProviderConfigSpec defines the desired state of a ProviderConfig.
type ProviderConfigSpec struct {
// Credentials required to authenticate to this provider.
// You may set source: None to rely on env/file config.
Credentials ProviderCredentials `json:"credentials"`

// Scw controls how the Scaleway shared config is discovered and selected.
// Optional; if omitted, file discovery is enabled by default and the SDK
// will use its active profile (env can still override).
Scw *ScwConfig `json:"scw,omitempty"`
}

// ProviderCredentials required to authenticate.
Expand All @@ -25,6 +31,18 @@ type ProviderCredentials struct {
xpv1.CommonCredentialSelectors `json:",inline"`
}

type ScwConfig struct {
// UseScwConfig toggles loading of the SCW config file. Defaults to true if nil.
UseScwConfig *bool `json:"useScwConfig,omitempty"`

// Path to a specific config file. If unset, SDK discovery is used.
Path *string `json:"path,omitempty"`

// Profile name to select from the config file. If unset, SDK active profile is used.
// Note: SCW_* environment variables still override file values.
Profile *string `json:"profile,omitempty"`
}

// A ProviderConfigStatus reflects the observed state of a ProviderConfig.
type ProviderConfigStatus struct {
xpv1.ProviderConfigStatus `json:",inline"`
Expand Down
35 changes: 35 additions & 0 deletions apis/v1beta1/zz_generated.deepcopy.go

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

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/crossplane/crossplane-tools v0.0.0-20230925130601-628280f8bf79
github.com/crossplane/upjet v1.9.0
github.com/pkg/errors v0.9.1
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35
golang.org/x/text v0.30.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.34.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
Expand Down
158 changes: 135 additions & 23 deletions internal/clients/scaleway.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/pkg/errors"
"github.com/scaleway/crossplane-provider-scaleway/internal/version"
"github.com/scaleway/scaleway-sdk-go/scw"

"github.com/crossplane/upjet/pkg/terraform"

Expand All @@ -27,15 +28,18 @@ const (
errNoProviderConfig = "no providerConfigRef provided"
errGetProviderConfig = "cannot get referenced ProviderConfig"
errTrackUsage = "cannot track ProviderConfig usage"
errExtractCredentials = "cannot extract credentials"
errUnmarshalCredentials = "cannot unmarshal scaleway credentials as JSON"
errLoadSCWConfig = "cannot load SCW config file"
errGetSCWProfile = "cannot get SCW profile from config"

keyAccessKey = "access_key"
keySecretKey = "secret_key"
keyProjectID = "project_id"
keyOrganizationID = "organization_id"
keyRegion = "region"
keyZone = "zone"
keyAPIURL = "api_url"
keyInsecure = "insecure"
)

// TerraformSetupBuilder builds Terraform a terraform.SetupFn function which
Expand All @@ -48,6 +52,7 @@ func TerraformSetupBuilder(tfversion, providerSource, providerVersion string) te
Source: providerSource,
Version: providerVersion,
},
Configuration: map[string]any{},
}

configRef := mg.GetProviderConfigReference()
Expand All @@ -64,32 +69,21 @@ func TerraformSetupBuilder(tfversion, providerSource, providerVersion string) te
return ps, errors.Wrap(err, errTrackUsage)
}

data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
// Load Scaleway config file + Env (Env > File)
profile, err := resolveScwProfile(pc)
if err != nil {
return ps, errors.Wrap(err, errExtractCredentials)
}
creds := map[string]string{}
if err := json.Unmarshal(data, &creds); err != nil {
return ps, errors.Wrap(err, errUnmarshalCredentials)
}

scalewayCreds := map[string]string{}
if err := json.Unmarshal(data, &scalewayCreds); err != nil {
return ps, errors.Wrap(err, errUnmarshalCredentials)
return ps, err
}
fillConfigFromProfile(ps.Configuration, profile)

ps.Configuration = map[string]interface{}{}
for _, key := range []string{
keyAccessKey,
keySecretKey,
keyProjectID,
keyOrganizationID,
keyRegion,
keyZone,
} {
if scalewayCreds[key] != "" {
ps.Configuration[key] = scalewayCreds[key]
// Overlay Secret (if any). Secret > Env > File
data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
if err == nil && len(data) > 0 {
vals := map[string]string{}
if err := json.Unmarshal(data, &vals); err != nil {
return ps, errors.Wrap(err, errUnmarshalCredentials)
}
overlayCredentials(ps.Configuration, vals)
}

// Set the custom user agent
Expand All @@ -102,3 +96,121 @@ func TerraformSetupBuilder(tfversion, providerSource, providerVersion string) te
return ps, nil
}
}

// overlayCredentials overlays non-empty Secret values over existing config
func overlayCredentials(cfg map[string]any, vals map[string]string) {
for _, k := range []string{
keyAccessKey, keySecretKey, keyProjectID, keyOrganizationID,
keyRegion, keyZone, keyAPIURL, keyInsecure,
} {
if v, ok := vals[k]; ok && v != "" {
cfg[k] = v
}
}
}

func resolveScwProfile(pc *v1beta1.ProviderConfig) (*scw.Profile, error) {
useFile := shouldUseScwConfig(pc)

fileProf, err := readScwConfigProfile(pc, useFile)
if err != nil {
return nil, err
}

envProf := scw.LoadEnvProfile()
return scw.MergeProfiles(fileProf, envProf), nil
}

func readScwConfigProfile(pc *v1beta1.ProviderConfig, useFile bool) (*scw.Profile, error) {
if !useFile {
return nil, nil
}

cfg, err := loadScwConfigFile(pc)
if err != nil {
return nil, err
}
if cfg == nil {
return nil, nil
}

return selectScwProfile(pc, cfg)
}

func loadScwConfigFile(pc *v1beta1.ProviderConfig) (*scw.Config, error) {
var cfg *scw.Config
var err error

if hasScwPath(pc) {
cfg, err = scw.LoadConfigFromPath(*pc.Spec.Scw.Path)
} else {
cfg, err = scw.LoadConfig()
}

var notFound *scw.ConfigFileNotFoundError
if err != nil && !errors.As(err, &notFound) {
return nil, errors.Wrap(err, errLoadSCWConfig)
}

return cfg, nil
}

func selectScwProfile(pc *v1beta1.ProviderConfig, cfg *scw.Config) (*scw.Profile, error) {
if cfg == nil {
return nil, nil
}

if hasScwProfile(pc) {
prof, err := cfg.GetProfile(*pc.Spec.Scw.Profile)
if err != nil {
return nil, errors.Wrap(err, errGetSCWProfile)
}
return prof, nil
}

prof, err := cfg.GetActiveProfile()
if err != nil {
return nil, errors.Wrap(err, errGetSCWProfile)
}
return prof, nil
}

func hasScwPath(pc *v1beta1.ProviderConfig) bool {
return pc.Spec.Scw != nil && pc.Spec.Scw.Path != nil && *pc.Spec.Scw.Path != ""
}

func hasScwProfile(pc *v1beta1.ProviderConfig) bool {
return pc.Spec.Scw != nil && pc.Spec.Scw.Profile != nil && *pc.Spec.Scw.Profile != ""
}

func shouldUseScwConfig(pc *v1beta1.ProviderConfig) bool {
if pc.Spec.Scw != nil && pc.Spec.Scw.UseScwConfig != nil {
return *pc.Spec.Scw.UseScwConfig
}
return true
}

// fillConfigFromProfile copies non-empty profile fields into cfg
func fillConfigFromProfile(cfg map[string]any, p *scw.Profile) {
if cfg == nil || p == nil {
return
}

assign := func(key string, val *string) {
if val != nil && *val != "" {
cfg[key] = *val
}
}

assign(keyAccessKey, p.AccessKey)
assign(keySecretKey, p.SecretKey)
assign(keyProjectID, p.DefaultProjectID)
assign(keyOrganizationID, p.DefaultOrganizationID)
assign(keyRegion, p.DefaultRegion)
assign(keyZone, p.DefaultZone)
assign(keyAPIURL, p.APIURL)

if p.Insecure != nil {
cfg[keyInsecure] = *p.Insecure
}
}
24 changes: 23 additions & 1 deletion package/crds/scaleway.upbound.io_providerconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ spec:
description: A ProviderConfigSpec defines the desired state of a ProviderConfig.
properties:
credentials:
description: Credentials required to authenticate to this provider.
description: |-
Credentials required to authenticate to this provider.
You may set source: None to rely on env/file config.
properties:
env:
description: |-
Expand Down Expand Up @@ -107,6 +109,26 @@ spec:
required:
- source
type: object
scw:
description: |-
Scw controls how the Scaleway shared config is discovered and selected.
Optional; if omitted, file discovery is enabled by default and the SDK
will use its active profile (env can still override).
properties:
path:
description: Path to a specific config file. If unset, SDK discovery
is used.
type: string
profile:
description: |-
Profile name to select from the config file. If unset, SDK active profile is used.
Note: SCW_* environment variables still override file values.
type: string
useScwConfig:
description: UseScwConfig toggles loading of the SCW config file.
Defaults to true if nil.
type: boolean
type: object
required:
- credentials
type: object
Expand Down
Loading