Skip to content

Commit 492048d

Browse files
committed
enables real life s3 provisioning
1 parent 7a1e06c commit 492048d

7 files changed

Lines changed: 365 additions & 24 deletions

File tree

demo/aws-sdk-go-v2/service/s3/client.go

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
// Package s3 is a minimal local fork of an AWS SDK package for the Runtime
2-
// Conditions demo. It intentionally mirrors the shape of an SDK client while
3-
// keeping runtime behavior inert and dependency-free.
2+
// Conditions demo. It mirrors the shape of an SDK client and performs the
3+
// S3 PutObject call needed by the demo without pulling in the full AWS SDK.
44
package s3
55

66
import (
7+
"bytes"
78
"context"
9+
"crypto/hmac"
10+
"crypto/sha256"
11+
"encoding/hex"
812
"errors"
13+
"fmt"
914
"io"
15+
"net/http"
16+
"net/url"
1017
"os"
18+
"strings"
19+
"time"
1120
)
1221

1322
// Config represents SDK configuration.
@@ -39,10 +48,124 @@ func (c *Client) PutObject(ctx context.Context, input *PutObjectInput, optFns ..
3948
if input == nil || input.Bucket == nil || *input.Bucket == "" {
4049
return nil, errors.New("s3 bucket is required")
4150
}
42-
for _, key := range []string{"AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"} {
43-
if os.Getenv(key) == "" {
44-
return nil, errors.New(key + " is not set")
51+
if input.Key == nil || *input.Key == "" {
52+
return nil, errors.New("s3 object key is required")
53+
}
54+
region, accessKeyID, secretAccessKey, err := credentialsFromEnv()
55+
if err != nil {
56+
return nil, err
57+
}
58+
var body []byte
59+
if input.Body == nil {
60+
body = nil
61+
} else {
62+
body, err = io.ReadAll(input.Body)
63+
if err != nil {
64+
return nil, fmt.Errorf("read s3 body: %w", err)
4565
}
4666
}
67+
68+
host := fmt.Sprintf("%s.s3.%s.amazonaws.com", *input.Bucket, region)
69+
canonicalURI := "/" + encodeObjectKey(*input.Key)
70+
endpoint := "https://" + host + canonicalURI
71+
request, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))
72+
if err != nil {
73+
return nil, err
74+
}
75+
signS3Request(request, body, host, region, accessKeyID, secretAccessKey, time.Now().UTC())
76+
77+
response, err := http.DefaultClient.Do(request)
78+
if err != nil {
79+
return nil, fmt.Errorf("s3 PutObject failed: %w", err)
80+
}
81+
defer response.Body.Close()
82+
if response.StatusCode < 200 || response.StatusCode > 299 {
83+
responseBody, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
84+
return nil, fmt.Errorf("s3 PutObject returned %s: %s", response.Status, strings.TrimSpace(string(responseBody)))
85+
}
4786
return &PutObjectOutput{}, nil
4887
}
88+
89+
func credentialsFromEnv() (string, string, string, error) {
90+
region := os.Getenv("AWS_REGION")
91+
accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
92+
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
93+
for key, value := range map[string]string{
94+
"AWS_REGION": region,
95+
"AWS_ACCESS_KEY_ID": accessKeyID,
96+
"AWS_SECRET_ACCESS_KEY": secretAccessKey,
97+
} {
98+
if value == "" {
99+
return "", "", "", errors.New(key + " is not set")
100+
}
101+
}
102+
return region, accessKeyID, secretAccessKey, nil
103+
}
104+
105+
func signS3Request(request *http.Request, body []byte, host, region, accessKeyID, secretAccessKey string, now time.Time) {
106+
payloadHash := sha256Hex(body)
107+
amzDate := now.Format("20060102T150405Z")
108+
shortDate := now.Format("20060102")
109+
credentialScope := shortDate + "/" + region + "/s3/aws4_request"
110+
signedHeaders := "host;x-amz-content-sha256;x-amz-date"
111+
112+
request.Header.Set("X-Amz-Content-Sha256", payloadHash)
113+
request.Header.Set("X-Amz-Date", amzDate)
114+
115+
canonicalHeaders := "host:" + host + "\n" +
116+
"x-amz-content-sha256:" + payloadHash + "\n" +
117+
"x-amz-date:" + amzDate + "\n"
118+
canonicalRequest := strings.Join(
119+
[]string{
120+
request.Method,
121+
request.URL.EscapedPath(),
122+
"",
123+
canonicalHeaders,
124+
signedHeaders,
125+
payloadHash,
126+
},
127+
"\n",
128+
)
129+
stringToSign := strings.Join(
130+
[]string{
131+
"AWS4-HMAC-SHA256",
132+
amzDate,
133+
credentialScope,
134+
sha256Hex([]byte(canonicalRequest)),
135+
},
136+
"\n",
137+
)
138+
signature := hex.EncodeToString(hmacSHA256(signingKey(secretAccessKey, shortDate, region), []byte(stringToSign)))
139+
request.Header.Set(
140+
"Authorization",
141+
"AWS4-HMAC-SHA256 Credential="+accessKeyID+"/"+credentialScope+
142+
", SignedHeaders="+signedHeaders+
143+
", Signature="+signature,
144+
)
145+
}
146+
147+
func signingKey(secretAccessKey, shortDate, region string) []byte {
148+
dateKey := hmacSHA256([]byte("AWS4"+secretAccessKey), []byte(shortDate))
149+
regionKey := hmacSHA256(dateKey, []byte(region))
150+
serviceKey := hmacSHA256(regionKey, []byte("s3"))
151+
return hmacSHA256(serviceKey, []byte("aws4_request"))
152+
}
153+
154+
func hmacSHA256(key, value []byte) []byte {
155+
hash := hmac.New(sha256.New, key)
156+
hash.Write(value)
157+
return hash.Sum(nil)
158+
}
159+
160+
func sha256Hex(value []byte) string {
161+
sum := sha256.Sum256(value)
162+
return hex.EncodeToString(sum[:])
163+
}
164+
165+
func encodeObjectKey(key string) string {
166+
parts := strings.Split(key, "/")
167+
for index, part := range parts {
168+
parts[index] = url.PathEscape(part)
169+
}
170+
return strings.Join(parts, "/")
171+
}

docs/guides/generator-discovery-workflow.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ The profile records the environment variable names expected by the workload. It
175175
| ---- | ---- |
176176
| `baseUrl` | Literal service URL from the API catalog |
177177
| `url`, `hostname`, `port` | Redis Promise connection ConfigMap |
178-
| `bucket`, `region` | S3Bucket Promise connection ConfigMap |
179-
| `accessKeyId`, `secretAccessKey` | S3Bucket Promise credentials Secret |
178+
| `bucket`, `region` | S3Bucket Promise connection ConfigMap for the provisioned AWS bucket |
179+
| `accessKeyId`, `secretAccessKey` | S3Bucket Promise credentials Secret for the generated bucket-scoped IAM access key |
180180

181181
The same adapter emits Cilium policy requests for the resolved workload:
182182

extensions/aws-object-store/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,4 @@ The application imports and calls the SDK normally. Runtime Conditions tooling
110110
reads the package manifest for direct imports and emits the profile condition
111111
without requiring application code to import a separate declaration package.
112112

113-
In the Kratix demo, the runtime-workload adapter provisions an `S3Bucket` request. The S3Bucket Promise publishes non-sensitive connection properties through a ConfigMap and sensitive credential properties through a Secret. The adapter uses the profile's `configuration.env[].name` values to wire those provider outputs into the workload Deployment.
113+
In the Kratix demo, the runtime-workload adapter provisions an `S3Bucket` request. The S3Bucket Promise creates a real AWS S3 bucket and a bucket-scoped IAM access key using platform-owned AWS admin credentials. It publishes non-sensitive connection properties through a ConfigMap and sensitive workload credentials through a Secret. The adapter uses the profile's `configuration.env[].name` values to wire those provider outputs into the workload Deployment.

platform/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ The scripts assume:
1212

1313
The Kratix installer used here is the OSS quick-start installer. It is suitable for this executable proof, not for a hardened long-lived production platform.
1414

15+
## AWS Credentials
16+
17+
The `S3Bucket` Promise provisions a real AWS S3 bucket and a bucket-scoped IAM user/access key for the workload. Before installing the Promises, create an AWS admin credential Secret in the Kratix platform namespace:
18+
19+
```sh
20+
kubectl -n kratix-platform-system create secret generic aws-admin-credentials \
21+
--from-literal=ACCESS_KEY_ID=<aws-access-key-id> \
22+
--from-literal=SECRET_ACCESS_KEY=<aws-secret-access-key>
23+
```
24+
25+
The pipeline reads those values as admin credentials, then writes separate workload credentials into the application namespace through the generated `S3Bucket` request.
26+
1527
## Quick Run
1628

1729
From the repository root:
@@ -123,8 +135,8 @@ The `RuntimeWorkload` Promise adapter maps declared properties to provider outpu
123135
| --- | --- |
124136
| `api` `baseUrl` | Literal service URL from the API catalog |
125137
| Redis `url`, `hostname`, `port` | Redis Promise connection ConfigMap |
126-
| S3 `bucket`, `region` | S3Bucket Promise connection ConfigMap |
127-
| S3 `accessKeyId`, `secretAccessKey` | S3Bucket Promise credentials Secret |
138+
| S3 `bucket`, `region` | S3Bucket Promise connection ConfigMap backed by a real AWS S3 bucket |
139+
| S3 `accessKeyId`, `secretAccessKey` | S3Bucket Promise credentials Secret backed by a bucket-scoped IAM access key |
128140

129141
This keeps the Kratix Promises reusable outside the adapter. The Redis and S3Bucket Promises publish generic connection artifacts, while the RuntimeWorkload adapter performs the profile-specific binding into a Kubernetes `Deployment`.
130142

platform/kratix/promises/s3-bucket/pipeline/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.12-alpine
22

3-
RUN pip install --no-cache-dir PyYAML==6.0.2
3+
RUN pip install --no-cache-dir PyYAML==6.0.2 boto3==1.35.82
44

55
COPY configure.py /usr/local/bin/configure.py
66

0 commit comments

Comments
 (0)