Skip to content

Commit 47ddfb2

Browse files
deekay2310claude
authored andcommitted
feat(ibmcloud): add IBM COS S3-compatible backend support
Add provider-specific env vars (IBMCLOUD_COS_ACCESS_KEY_ID, IBMCLOUD_COS_SECRET_ACCESS_KEY, IBMCLOUD_COS_ENDPOINT) and map them to AWS SDK env vars so the Pulumi S3 backend can authenticate to IBM Cloud Object Storage. Construct the full Pulumi backend URL with endpoint query params via Provider.Init() return value, ensuring Pulumi connects to COS instead of amazonaws.com. Also adds DestroyStack with lock cleanup, CleanupState for post-destroy state removal, --keep-state flag, empty key guard in parseS3BackedURL, and consolidates IBMCLOUD_* string constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a804179 commit 47ddfb2

14 files changed

Lines changed: 300 additions & 16 deletions

File tree

cmd/mapt/cmd/ibmcloud/hosts/ibm-power.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,14 @@ func ibmPowerDestroy() *cobra.Command {
101101
DebugLevel: viper.GetUint(params.DebugLevel),
102102
Serverless: viper.IsSet(params.Serverless),
103103
ForceDestroy: viper.IsSet(params.ForceDestroy),
104+
KeepState: viper.IsSet(params.KeepState),
104105
})
105106
},
106107
}
107108
flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError)
108109
flagSet.Bool(params.Serverless, false, params.ServerlessDesc)
109110
flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc)
111+
flagSet.Bool(params.KeepState, false, params.KeepStateDesc)
110112
c.PersistentFlags().AddFlagSet(flagSet)
111113
return c
112114
}

cmd/mapt/cmd/ibmcloud/hosts/ibm-z.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,14 @@ func ibmZDestroy() *cobra.Command {
9595
DebugLevel: viper.GetUint(params.DebugLevel),
9696
Serverless: viper.IsSet(params.Serverless),
9797
ForceDestroy: viper.IsSet(params.ForceDestroy),
98+
KeepState: viper.IsSet(params.KeepState),
9899
})
99100
},
100101
}
101102
flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError)
102103
flagSet.Bool(params.Serverless, false, params.ServerlessDesc)
103104
flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc)
105+
flagSet.Bool(params.KeepState, false, params.KeepStateDesc)
104106
c.PersistentFlags().AddFlagSet(flagSet)
105107
return c
106108
}

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/manager/context/context.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ type Context struct {
7474
}
7575

7676
type Provider interface {
77-
Init(ctx context.Context, backedURL string) error
77+
Init(ctx context.Context, backedURL string) (string, error)
7878
DefaultHostingPlace() (*string, error)
7979
}
8080

@@ -110,9 +110,13 @@ func Init(ca *ContextArgs, provider Provider) (*Context, error) {
110110
c.targetHostingPlace = *hp
111111
}
112112
// Manage
113-
if err := provider.Init(ctx, ca.BackedURL); err != nil {
113+
resolvedURL, err := provider.Init(ctx, ca.BackedURL)
114+
if err != nil {
114115
return nil, err
115116
}
117+
if resolvedURL != "" {
118+
c.backedURL = resolvedURL
119+
}
116120
// Manage integrations
117121
if err := manageIntegration(c, ca); err != nil {
118122
return nil, err

pkg/provider/aws/aws.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ const pulumiLocksPath = ".pulumi/locks"
2525

2626
type AWS struct{}
2727

28-
func (a *AWS) Init(ctx context.Context, backedURL string) error {
28+
func (a *AWS) Init(ctx context.Context, backedURL string) (string, error) {
2929
// Manage remote state requirements, if backedURL
3030
// is on a different region we need to change to that region
3131
// in order to interact with the state
32-
return manageRemoteState(ctx, backedURL)
32+
return "", manageRemoteState(ctx, backedURL)
3333
}
3434

3535
func (a *AWS) DefaultHostingPlace() (*string, error) {
@@ -217,6 +217,9 @@ func parseS3BackedURL(mCtx *mc.Context) (*string, *string, error) {
217217
return nil, nil, fmt.Errorf("failed to parse S3 URI: %w", err)
218218
}
219219
key := strings.TrimPrefix(u.Path, "/")
220+
if key == "" {
221+
return nil, nil, fmt.Errorf("invalid S3 URI %q: missing object key after bucket name", mCtx.BackedURL())
222+
}
220223
return &u.Host, &key, nil
221224
}
222225

pkg/provider/azure/azure.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ func Provider() *Azure {
2828
return &Azure{}
2929
}
3030

31-
func (a *Azure) Init(ctx context.Context, backedURL string) error {
31+
func (a *Azure) Init(ctx context.Context, backedURL string) (string, error) {
3232
setAZIdentityEnvs()
33-
return nil
33+
return "", nil
3434
}
3535

3636
func (a *Azure) DefaultHostingPlace() (*string, error) {

pkg/provider/ibmcloud/action/ibm-power/ibm-power.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ func Destroy(mCtxArgs *mc.ContextArgs) (err error) {
142142
if err != nil {
143143
return err
144144
}
145-
return ibmcloudp.Destroy(mCtx, stackIBMPowerVS)
145+
if err := ibmcloudp.DestroyStack(mCtx, stackIBMPowerVS); err != nil {
146+
return err
147+
}
148+
return ibmcloudp.CleanupState(mCtx)
146149
}
147150

148151
func (r *pwRequest) deploy(ctx *pulumi.Context) error {

pkg/provider/ibmcloud/action/ibm-z/ibm-z.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ func Destroy(mCtxArgs *mc.ContextArgs) (err error) {
136136
if err != nil {
137137
return err
138138
}
139-
return ibmcloudp.Destroy(mCtx, stackIBMS390)
139+
if err := ibmcloudp.DestroyStack(mCtx, stackIBMS390); err != nil {
140+
return err
141+
}
142+
return ibmcloudp.CleanupState(mCtx)
140143
}
141144

142145
func (r *zRequest) deploy(ctx *pulumi.Context) error {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package constants
2+
3+
const (
4+
EnvIBMCloudAccount = "IBMCLOUD_ACCOUNT"
5+
EnvIBMCloudAPIKey = "IBMCLOUD_API_KEY"
6+
EnvIBMCosAccessKeyID = "IBMCLOUD_COS_ACCESS_KEY_ID"
7+
EnvIBMCosSecretAccessKey = "IBMCLOUD_COS_SECRET_ACCESS_KEY"
8+
EnvIBMCosEndpoint = "IBMCLOUD_COS_ENDPOINT"
9+
)

0 commit comments

Comments
 (0)