Skip to content

Commit e5f6dae

Browse files
deekay2310claude
andcommitted
refactor(ibmcloud): construct PULUMI_BACKEND_URL and support HTTPS COS URLs
Rename isS3Path/manageCOSRemoteState/parseS3BackedURL to isCOSBackend/initCOSBackend/parseCOSBackedURL. Construct PULUMI_BACKEND_URL with endpoint query params so Pulumi can connect to COS without manual env var setup. Support both s3://bucket and HTTPS COS endpoint URLs as --backed-url. Document COS env vars, S3 backend usage, and --keep-state flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b9f4910 commit e5f6dae

3 files changed

Lines changed: 140 additions & 12 deletions

File tree

docs/ibmcloud/ibm-power.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ On first boot, cloud-init automatically configures the PowerVS instance for on-p
2424
| `IBMCLOUD_ACCOUNT` | yes | IBM Cloud account ID |
2525
| `IBMCLOUD_API_KEY` | yes | IBM Cloud API key |
2626
| `IC_REGION` | yes | IBM Cloud region (e.g. `us-south`, `us-east`) |
27+
| `IBMCLOUD_COS_ACCESS_KEY_ID` | only with S3 `--backed-url` | HMAC access key for IBM Cloud Object Storage |
28+
| `IBMCLOUD_COS_SECRET_ACCESS_KEY` | only with S3 `--backed-url` | HMAC secret key for IBM Cloud Object Storage |
29+
| `IBMCLOUD_COS_ENDPOINT` | no | COS S3 endpoint (defaults to `s3.<region>.cloud-object-storage.appdomain.cloud`) |
2730

2831
## Create
2932

@@ -134,6 +137,34 @@ podman run -d --name ibm-power \
134137
--otel-auth-token <uuid-token>
135138
```
136139

140+
## Using IBM Cloud Object Storage as S3 backend
141+
142+
To store Pulumi state in IBM COS instead of a local file, create [HMAC credentials](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-uhc-hmac-credentials-main) for your COS instance and pass an `s3://` backed URL:
143+
144+
```bash
145+
podman run -d --name ibm-power \
146+
-v ${PWD}:/workspace:z \
147+
-e IBMCLOUD_API_KEY=XXX \
148+
-e IBMCLOUD_ACCOUNT=XXX \
149+
-e IC_REGION=us-south \
150+
-e IBMCLOUD_COS_ACCESS_KEY_ID=XXX \
151+
-e IBMCLOUD_COS_SECRET_ACCESS_KEY=XXX \
152+
quay.io/redhat-developer/mapt:v0.8.0 ibmcloud ibm-power create \
153+
--project-name ibm-power \
154+
--backed-url s3://my-cos-bucket \
155+
--conn-details-output /workspace \
156+
--workspace-id <workspace-id> \
157+
--pi-private-subnet-id <private-subnet-id>
158+
```
159+
160+
An HTTPS endpoint URL is also supported as `--backed-url`, with the bucket name in the path:
161+
162+
```
163+
--backed-url https://s3.us-south.cloud-object-storage.appdomain.cloud/my-cos-bucket
164+
```
165+
166+
The COS endpoint and `PULUMI_BACKEND_URL` are constructed automatically from the region and bucket name.
167+
137168
## Destroy
138169

139170
```bash
@@ -144,4 +175,20 @@ podman run -d --name ibm-power \
144175
quay.io/redhat-developer/mapt:v0.8.0 ibmcloud ibm-power destroy \
145176
--project-name ibm-power \
146177
--backed-url file:///workspace
178+
```
179+
180+
By default, destroy removes the Pulumi state files from the backend after a successful destroy. Use `--keep-state` to preserve them:
181+
182+
```bash
183+
podman run -d --name ibm-power \
184+
-v ${PWD}:/workspace:z \
185+
-e IBMCLOUD_API_KEY=XXX \
186+
-e IBMCLOUD_ACCOUNT=XXX \
187+
-e IC_REGION=us-south \
188+
-e IBMCLOUD_COS_ACCESS_KEY_ID=XXX \
189+
-e IBMCLOUD_COS_SECRET_ACCESS_KEY=XXX \
190+
quay.io/redhat-developer/mapt:v0.8.0 ibmcloud ibm-power destroy \
191+
--project-name ibm-power \
192+
--backed-url s3://my-cos-bucket \
193+
--keep-state
147194
```

docs/ibmcloud/ibm-z.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Two networking modes are supported:
1515
| `IBMCLOUD_API_KEY` | yes | IBM Cloud API key |
1616
| `IC_REGION` | yes | IBM Cloud region (e.g. `us-south`, `us-east`) |
1717
| `IC_ZONE` | only without `--subnet-id` | Availability zone (e.g. `us-south-2`) |
18+
| `IBMCLOUD_COS_ACCESS_KEY_ID` | only with S3 `--backed-url` | HMAC access key for IBM Cloud Object Storage |
19+
| `IBMCLOUD_COS_SECRET_ACCESS_KEY` | only with S3 `--backed-url` | HMAC secret key for IBM Cloud Object Storage |
20+
| `IBMCLOUD_COS_ENDPOINT` | no | COS S3 endpoint (defaults to `s3.<region>.cloud-object-storage.appdomain.cloud`) |
1821

1922
## Create
2023

@@ -112,6 +115,33 @@ podman run -d --name ibm-z \
112115
--otel-auth-token <uuid-token>
113116
```
114117

118+
## Using IBM Cloud Object Storage as S3 backend
119+
120+
To store Pulumi state in IBM COS instead of a local file, create [HMAC credentials](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-uhc-hmac-credentials-main) for your COS instance and pass an `s3://` backed URL:
121+
122+
```bash
123+
podman run -d --name ibm-z \
124+
-v ${PWD}:/workspace:z \
125+
-e IBMCLOUD_API_KEY=XXX \
126+
-e IBMCLOUD_ACCOUNT=XXX \
127+
-e IC_REGION=us-south \
128+
-e IC_ZONE=us-south-2 \
129+
-e IBMCLOUD_COS_ACCESS_KEY_ID=XXX \
130+
-e IBMCLOUD_COS_SECRET_ACCESS_KEY=XXX \
131+
quay.io/redhat-developer/mapt:v0.8.0 ibmcloud ibm-z create \
132+
--project-name ibm-z \
133+
--backed-url s3://my-cos-bucket \
134+
--conn-details-output /workspace
135+
```
136+
137+
An HTTPS endpoint URL is also supported as `--backed-url`, with the bucket name in the path:
138+
139+
```
140+
--backed-url https://s3.us-south.cloud-object-storage.appdomain.cloud/my-cos-bucket
141+
```
142+
143+
The COS endpoint and `PULUMI_BACKEND_URL` are constructed automatically from the region and bucket name.
144+
115145
## Destroy
116146

117147
```bash
@@ -123,3 +153,19 @@ podman run -d --name ibm-z \
123153
--project-name ibm-z \
124154
--backed-url file:///workspace
125155
```
156+
157+
By default, destroy removes the Pulumi state files from the backend after a successful destroy. Use `--keep-state` to preserve them:
158+
159+
```bash
160+
podman run -d --name ibm-z \
161+
-v ${PWD}:/workspace:z \
162+
-e IBMCLOUD_API_KEY=XXX \
163+
-e IBMCLOUD_ACCOUNT=XXX \
164+
-e IC_REGION=us-south \
165+
-e IBMCLOUD_COS_ACCESS_KEY_ID=XXX \
166+
-e IBMCLOUD_COS_SECRET_ACCESS_KEY=XXX \
167+
quay.io/redhat-developer/mapt:v0.8.0 ibmcloud ibm-z destroy \
168+
--project-name ibm-z \
169+
--backed-url s3://my-cos-bucket \
170+
--keep-state
171+
```

pkg/provider/ibmcloud/ibmcloud.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ const (
2323
type IBMCloud struct{}
2424

2525
func (i *IBMCloud) Init(ctx context.Context, backedURL string) error {
26-
if isS3Path(backedURL) {
27-
return manageCOSRemoteState(backedURL)
26+
if isCOSBackend(backedURL) {
27+
return initCOSBackend(backedURL)
2828
}
2929
return nil
3030
}
@@ -59,8 +59,11 @@ var (
5959
DefaultCredentials = GetClouProviderCredentials(nil)
6060
)
6161

62-
func isS3Path(backedURL string) bool {
63-
return strings.HasPrefix(backedURL, "s3://")
62+
const cosHostSuffix = "cloud-object-storage.appdomain.cloud"
63+
64+
func isCOSBackend(backedURL string) bool {
65+
return strings.HasPrefix(backedURL, "s3://") ||
66+
strings.Contains(backedURL, cosHostSuffix)
6467
}
6568

6669
func ensureHTTPS(endpoint string) string {
@@ -82,7 +85,25 @@ func requireEnv(name string) (string, error) {
8285
return v, nil
8386
}
8487

85-
func manageCOSRemoteState(backedURL string) error {
88+
func extractBucket(backedURL string) (string, error) {
89+
u, err := url.Parse(backedURL)
90+
if err != nil {
91+
return "", fmt.Errorf("failed to parse backed URL %q: %w", backedURL, err)
92+
}
93+
if strings.HasPrefix(backedURL, "s3://") {
94+
if u.Host == "" {
95+
return "", fmt.Errorf("backed URL %q missing bucket name (expected s3://bucket-name)", backedURL)
96+
}
97+
return u.Host, nil
98+
}
99+
bucket := strings.TrimPrefix(u.Path, "/")
100+
if bucket == "" {
101+
return "", fmt.Errorf("backed URL %q missing bucket name in path (expected https://<endpoint>/<bucket>)", backedURL)
102+
}
103+
return strings.SplitN(bucket, "/", 2)[0], nil
104+
}
105+
106+
func initCOSBackend(backedURL string) error {
86107
accessKey, err := requireEnv(icConstants.EnvIBMCosAccessKeyID)
87108
if err != nil {
88109
return err
@@ -101,32 +122,46 @@ func manageCOSRemoteState(backedURL string) error {
101122
endpoint = fmt.Sprintf("s3.%s.cloud-object-storage.appdomain.cloud", region)
102123
}
103124

125+
bucket, err := extractBucket(backedURL)
126+
if err != nil {
127+
return err
128+
}
129+
130+
pulumiBackendURL := fmt.Sprintf("s3://%s?endpoint=%s&s3ForcePathStyle=true",
131+
bucket, ensureHTTPS(endpoint))
132+
104133
for k, v := range map[string]string{
105134
"AWS_ACCESS_KEY_ID": accessKey,
106135
"AWS_SECRET_ACCESS_KEY": secretKey,
107136
"AWS_ENDPOINT_URL": ensureHTTPS(endpoint),
108137
"AWS_REGION": region,
109138
"AWS_DEFAULT_REGION": region,
110139
"AWS_S3_USE_PATH_STYLE": "true",
140+
"PULUMI_BACKEND_URL": pulumiBackendURL,
111141
} {
112142
if err := os.Setenv(k, v); err != nil {
113143
return err
114144
}
115145
}
146+
logging.Debugf("COS backend configured: %s", pulumiBackendURL)
116147
return nil
117148
}
118149

119-
func parseS3BackedURL(mCtx *mc.Context) (*string, *string, error) {
120-
if !strings.HasPrefix(mCtx.BackedURL(), "s3://") {
121-
return nil, nil, fmt.Errorf("invalid S3 URI: must start with s3://")
150+
func parseCOSBackedURL(mCtx *mc.Context) (*string, *string, error) {
151+
backendURL := os.Getenv("PULUMI_BACKEND_URL")
152+
if backendURL == "" {
153+
backendURL = mCtx.BackedURL()
154+
}
155+
if !strings.HasPrefix(backendURL, "s3://") {
156+
return nil, nil, fmt.Errorf("invalid S3 URI %q: must start with s3://", backendURL)
122157
}
123-
u, err := url.Parse(mCtx.BackedURL())
158+
u, err := url.Parse(backendURL)
124159
if err != nil {
125160
return nil, nil, fmt.Errorf("failed to parse S3 URI: %w", err)
126161
}
127162
key := strings.TrimPrefix(u.Path, "/")
128163
if key == "" {
129-
return nil, nil, fmt.Errorf("invalid S3 URI %q: missing object key after bucket name", mCtx.BackedURL())
164+
return nil, nil, fmt.Errorf("invalid S3 URI %q: missing object key after bucket name", backendURL)
130165
}
131166
return &u.Host, &key, nil
132167
}
@@ -137,7 +172,7 @@ func DestroyStack(mCtx *mc.Context, stackName string) error {
137172
return fmt.Errorf("stackname is required")
138173
}
139174
if mCtx.IsForceDestroy() {
140-
bucket, key, err := parseS3BackedURL(mCtx)
175+
bucket, key, err := parseCOSBackedURL(mCtx)
141176
if err != nil {
142177
logging.Error(err)
143178
} else {
@@ -161,7 +196,7 @@ func CleanupState(mCtx *mc.Context) error {
161196
return nil
162197
}
163198

164-
bucket, key, parseErr := parseS3BackedURL(mCtx)
199+
bucket, key, parseErr := parseCOSBackedURL(mCtx)
165200
if parseErr != nil {
166201
logging.Warnf("Failed to parse S3 backend URL, skipping state cleanup: %v", parseErr)
167202
return nil

0 commit comments

Comments
 (0)