create-svc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/index.ts +5 -0
- package/package.json +48 -0
- package/src/cli.ts +300 -0
- package/src/scaffold.test.ts +46 -0
- package/src/scaffold.ts +133 -0
- package/templates/root/.github/workflows/buf-publish.yml +19 -0
- package/templates/root/.github/workflows/ci.yml +26 -0
- package/templates/root/.github/workflows/deploy.yml +22 -0
- package/templates/root/Dockerfile +23 -0
- package/templates/root/README.md +69 -0
- package/templates/root/buf.gen.yaml +10 -0
- package/templates/root/buf.yaml +9 -0
- package/templates/root/cmd/server/main.go +44 -0
- package/templates/root/gen/dns/v1/dns.pb.go +623 -0
- package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
- package/templates/root/go.mod +10 -0
- package/templates/root/internal/app/service.go +152 -0
- package/templates/root/internal/app/token_source.go +50 -0
- package/templates/root/internal/cloudflare/client.go +160 -0
- package/templates/root/internal/config/config.go +55 -0
- package/templates/root/internal/connectapi/handler.go +79 -0
- package/templates/root/internal/httpapi/routes.go +93 -0
- package/templates/root/internal/vault/client.go +148 -0
- package/templates/root/package.json +12 -0
- package/templates/root/protos/dns/v1/dns.proto +58 -0
- package/templates/root/scripts/cloudrun/bootstrap.ts +65 -0
- package/templates/root/scripts/cloudrun/config.ts +50 -0
- package/templates/root/scripts/cloudrun/deploy.ts +41 -0
- package/templates/root/scripts/cloudrun/lib.ts +244 -0
- package/templates/root/service.yaml +50 -0
- package/templates/root/test/go.test.ts +19 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package httpapi
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"net/http"
|
|
7
|
+
"strconv"
|
|
8
|
+
"strings"
|
|
9
|
+
|
|
10
|
+
"github.com/go-chi/chi/v5"
|
|
11
|
+
|
|
12
|
+
"{{MODULE_PATH}}/internal/app"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func RegisterRoutes(router chi.Router, service *app.DNSService) {
|
|
16
|
+
router.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
17
|
+
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
router.Route("/v1/dns/records", func(r chi.Router) {
|
|
21
|
+
r.Get("/", func(w http.ResponseWriter, request *http.Request) {
|
|
22
|
+
records, err := service.ListRecords(request.Context())
|
|
23
|
+
if err != nil {
|
|
24
|
+
writeError(w, err)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
writeJSON(w, http.StatusOK, map[string]any{"records": records})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
r.Post("/", func(w http.ResponseWriter, request *http.Request) {
|
|
31
|
+
var input app.CreateRecordInput
|
|
32
|
+
if err := decodeJSON(request, &input); err != nil {
|
|
33
|
+
writeError(w, err)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
record, err := service.CreateRecord(request.Context(), input)
|
|
38
|
+
if err != nil {
|
|
39
|
+
writeError(w, err)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
writeJSON(w, http.StatusCreated, map[string]any{"record": record})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
r.Route("/{recordID}", func(r chi.Router) {
|
|
46
|
+
r.Put("/", func(w http.ResponseWriter, request *http.Request) {
|
|
47
|
+
var input app.UpdateRecordInput
|
|
48
|
+
if err := decodeJSON(request, &input); err != nil {
|
|
49
|
+
writeError(w, err)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
record, err := service.UpdateRecord(request.Context(), chi.URLParam(request, "recordID"), input)
|
|
54
|
+
if err != nil {
|
|
55
|
+
writeError(w, err)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
writeJSON(w, http.StatusOK, map[string]any{"record": record})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
r.Delete("/", func(w http.ResponseWriter, request *http.Request) {
|
|
62
|
+
if err := service.DeleteRecord(request.Context(), chi.URLParam(request, "recordID")); err != nil {
|
|
63
|
+
writeError(w, err)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
w.WriteHeader(http.StatusNoContent)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func decodeJSON(request *http.Request, out any) error {
|
|
73
|
+
defer request.Body.Close()
|
|
74
|
+
|
|
75
|
+
if err := json.NewDecoder(request.Body).Decode(out); err != nil {
|
|
76
|
+
return err
|
|
77
|
+
}
|
|
78
|
+
return nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
82
|
+
w.Header().Set("Content-Type", "application/json")
|
|
83
|
+
w.WriteHeader(status)
|
|
84
|
+
_ = json.NewEncoder(w).Encode(payload)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func writeError(w http.ResponseWriter, err error) {
|
|
88
|
+
status := http.StatusInternalServerError
|
|
89
|
+
if errors.Is(err, strconv.ErrSyntax) || strings.Contains(strings.ToLower(err.Error()), "json") {
|
|
90
|
+
status = http.StatusBadRequest
|
|
91
|
+
}
|
|
92
|
+
writeJSON(w, status, map[string]string{"error": err.Error()})
|
|
93
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package vault
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"net/http"
|
|
10
|
+
"os"
|
|
11
|
+
"strings"
|
|
12
|
+
"sync"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type AppRoleClient struct {
|
|
16
|
+
addr string
|
|
17
|
+
roleIDFile string
|
|
18
|
+
secretIDFile string
|
|
19
|
+
client *http.Client
|
|
20
|
+
|
|
21
|
+
mu sync.Mutex
|
|
22
|
+
token string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func NewAppRoleClient(addr string, roleIDFile string, secretIDFile string, client *http.Client) *AppRoleClient {
|
|
26
|
+
return &AppRoleClient{
|
|
27
|
+
addr: strings.TrimRight(addr, "/"),
|
|
28
|
+
roleIDFile: roleIDFile,
|
|
29
|
+
secretIDFile: secretIDFile,
|
|
30
|
+
client: client,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func (c *AppRoleClient) Get(ctx context.Context, path string, key string) (string, error) {
|
|
35
|
+
token, err := c.login(ctx)
|
|
36
|
+
if err != nil {
|
|
37
|
+
return "", err
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/secret/data/%s", c.addr, strings.TrimLeft(path, "/")), nil)
|
|
41
|
+
if err != nil {
|
|
42
|
+
return "", err
|
|
43
|
+
}
|
|
44
|
+
request.Header.Set("X-Vault-Token", token)
|
|
45
|
+
|
|
46
|
+
response, err := c.client.Do(request)
|
|
47
|
+
if err != nil {
|
|
48
|
+
return "", err
|
|
49
|
+
}
|
|
50
|
+
defer response.Body.Close()
|
|
51
|
+
|
|
52
|
+
raw, err := io.ReadAll(response.Body)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return "", err
|
|
55
|
+
}
|
|
56
|
+
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
57
|
+
return "", fmt.Errorf("vault read failed: status=%d body=%s", response.StatusCode, strings.TrimSpace(string(raw)))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var payload struct {
|
|
61
|
+
Data struct {
|
|
62
|
+
Data map[string]string `json:"data"`
|
|
63
|
+
} `json:"data"`
|
|
64
|
+
}
|
|
65
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
66
|
+
return "", err
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
value := strings.TrimSpace(payload.Data.Data[key])
|
|
70
|
+
if value == "" {
|
|
71
|
+
return "", fmt.Errorf("vault secret key %q is empty", key)
|
|
72
|
+
}
|
|
73
|
+
return value, nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func (c *AppRoleClient) login(ctx context.Context) (string, error) {
|
|
77
|
+
c.mu.Lock()
|
|
78
|
+
defer c.mu.Unlock()
|
|
79
|
+
|
|
80
|
+
if c.token != "" {
|
|
81
|
+
return c.token, nil
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
roleID, err := readSecretFile(c.roleIDFile)
|
|
85
|
+
if err != nil {
|
|
86
|
+
return "", err
|
|
87
|
+
}
|
|
88
|
+
secretID, err := readSecretFile(c.secretIDFile)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return "", err
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
body, err := json.Marshal(map[string]string{
|
|
94
|
+
"role_id": roleID,
|
|
95
|
+
"secret_id": secretID,
|
|
96
|
+
})
|
|
97
|
+
if err != nil {
|
|
98
|
+
return "", err
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.addr+"/v1/auth/approle/login", bytes.NewReader(body))
|
|
102
|
+
if err != nil {
|
|
103
|
+
return "", err
|
|
104
|
+
}
|
|
105
|
+
request.Header.Set("Content-Type", "application/json")
|
|
106
|
+
|
|
107
|
+
response, err := c.client.Do(request)
|
|
108
|
+
if err != nil {
|
|
109
|
+
return "", err
|
|
110
|
+
}
|
|
111
|
+
defer response.Body.Close()
|
|
112
|
+
|
|
113
|
+
raw, err := io.ReadAll(response.Body)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return "", err
|
|
116
|
+
}
|
|
117
|
+
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
118
|
+
return "", fmt.Errorf("vault login failed: status=%d body=%s", response.StatusCode, strings.TrimSpace(string(raw)))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var payload struct {
|
|
122
|
+
Auth struct {
|
|
123
|
+
ClientToken string `json:"client_token"`
|
|
124
|
+
} `json:"auth"`
|
|
125
|
+
}
|
|
126
|
+
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
127
|
+
return "", err
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
c.token = strings.TrimSpace(payload.Auth.ClientToken)
|
|
131
|
+
if c.token == "" {
|
|
132
|
+
return "", fmt.Errorf("vault returned an empty client token")
|
|
133
|
+
}
|
|
134
|
+
return c.token, nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func readSecretFile(path string) (string, error) {
|
|
138
|
+
bytes, err := os.ReadFile(path)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return "", err
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
value := strings.TrimSpace(string(bytes))
|
|
144
|
+
if value == "" {
|
|
145
|
+
return "", fmt.Errorf("secret file %q is empty", path)
|
|
146
|
+
}
|
|
147
|
+
return value, nil
|
|
148
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{SERVICE_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "go run ./cmd/server",
|
|
7
|
+
"gen": "buf generate",
|
|
8
|
+
"lint": "go vet ./... && buf lint",
|
|
9
|
+
"bootstrap": "bun run ./scripts/cloudrun/bootstrap.ts",
|
|
10
|
+
"deploy": "bun run ./scripts/cloudrun/deploy.ts"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package dns.v1;
|
|
4
|
+
|
|
5
|
+
option go_package = "{{MODULE_PATH}}/gen/dns/v1;dnsv1";
|
|
6
|
+
|
|
7
|
+
service DNSService {
|
|
8
|
+
rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse) {}
|
|
9
|
+
rpc CreateRecord(CreateRecordRequest) returns (CreateRecordResponse) {}
|
|
10
|
+
rpc UpdateRecord(UpdateRecordRequest) returns (UpdateRecordResponse) {}
|
|
11
|
+
rpc DeleteRecord(DeleteRecordRequest) returns (DeleteRecordResponse) {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
message Record {
|
|
15
|
+
string id = 1;
|
|
16
|
+
string type = 2;
|
|
17
|
+
string name = 3;
|
|
18
|
+
string content = 4;
|
|
19
|
+
int32 ttl = 5;
|
|
20
|
+
bool proxied = 6;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
message ListRecordsRequest {}
|
|
24
|
+
|
|
25
|
+
message ListRecordsResponse {
|
|
26
|
+
repeated Record records = 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
message CreateRecordRequest {
|
|
30
|
+
string type = 1;
|
|
31
|
+
string name = 2;
|
|
32
|
+
string content = 3;
|
|
33
|
+
int32 ttl = 4;
|
|
34
|
+
bool proxied = 5;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
message CreateRecordResponse {
|
|
38
|
+
Record record = 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
message UpdateRecordRequest {
|
|
42
|
+
string id = 1;
|
|
43
|
+
string type = 2;
|
|
44
|
+
string name = 3;
|
|
45
|
+
string content = 4;
|
|
46
|
+
int32 ttl = 5;
|
|
47
|
+
bool proxied = 6;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
message UpdateRecordResponse {
|
|
51
|
+
Record record = 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
message DeleteRecordRequest {
|
|
55
|
+
string id = 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
message DeleteRecordResponse {}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { bootstrapSecrets, config } from "./config";
|
|
2
|
+
import {
|
|
3
|
+
ensureProjectRole,
|
|
4
|
+
ensureSecret,
|
|
5
|
+
ensureSecretAccessor,
|
|
6
|
+
ensureServiceAccount,
|
|
7
|
+
ensureServiceAccountRole,
|
|
8
|
+
ensureWorkloadIdentityPool,
|
|
9
|
+
ensureWorkloadIdentityProvider,
|
|
10
|
+
gcloud,
|
|
11
|
+
requireCommand,
|
|
12
|
+
setGithubSecret,
|
|
13
|
+
setGithubVariable,
|
|
14
|
+
workloadIdentityPoolResource,
|
|
15
|
+
workloadIdentityProviderResource,
|
|
16
|
+
} from "./lib";
|
|
17
|
+
|
|
18
|
+
export async function bootstrap() {
|
|
19
|
+
requireCommand("gcloud");
|
|
20
|
+
requireCommand("gh");
|
|
21
|
+
|
|
22
|
+
gcloud(["services", "enable", ...config.requiredApis, "--project", config.projectId]);
|
|
23
|
+
|
|
24
|
+
ensureServiceAccount(config.runtimeServiceAccount);
|
|
25
|
+
ensureServiceAccount(config.deployerServiceAccount);
|
|
26
|
+
|
|
27
|
+
ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/run.admin");
|
|
28
|
+
ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/cloudbuild.builds.editor");
|
|
29
|
+
ensureProjectRole(`serviceAccount:${config.deployerServiceAccount}`, "roles/serviceusage.serviceUsageConsumer");
|
|
30
|
+
|
|
31
|
+
ensureServiceAccountRole(config.runtimeServiceAccount, `serviceAccount:${config.deployerServiceAccount}`, "roles/iam.serviceAccountUser");
|
|
32
|
+
|
|
33
|
+
for (const secret of bootstrapSecrets) {
|
|
34
|
+
ensureSecret(secret.secretName, secret.bootstrapEnv);
|
|
35
|
+
ensureSecretAccessor(secret.secretName, `serviceAccount:${config.runtimeServiceAccount}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ensureWorkloadIdentityPool();
|
|
39
|
+
ensureWorkloadIdentityProvider();
|
|
40
|
+
|
|
41
|
+
ensureServiceAccountRole(
|
|
42
|
+
config.deployerServiceAccount,
|
|
43
|
+
`principalSet://iam.googleapis.com/${workloadIdentityPoolResource()}/attribute.repository/${config.githubRepo}`,
|
|
44
|
+
"roles/iam.workloadIdentityUser"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
for (const [name, value] of Object.entries(config.githubVariables)) {
|
|
48
|
+
setGithubVariable(name, value);
|
|
49
|
+
}
|
|
50
|
+
setGithubVariable("GCP_WIF_PROVIDER", workloadIdentityProviderResource());
|
|
51
|
+
setGithubVariable("GCP_DEPLOYER_SERVICE_ACCOUNT", config.deployerServiceAccount);
|
|
52
|
+
|
|
53
|
+
if (config.bufModule) {
|
|
54
|
+
setGithubVariable("BUF_MODULE", config.bufModule);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const bufToken = process.env.BUF_TOKEN?.trim();
|
|
58
|
+
if (bufToken && config.bufModule) {
|
|
59
|
+
setGithubSecret("BUF_TOKEN", bufToken);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (import.meta.main) {
|
|
64
|
+
await bootstrap();
|
|
65
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
serviceName: "{{SERVICE_NAME}}",
|
|
3
|
+
projectId: "{{PROJECT_ID}}",
|
|
4
|
+
region: "{{REGION}}",
|
|
5
|
+
githubRepo: "{{GITHUB_REPO}}",
|
|
6
|
+
bufModule: "{{BUF_MODULE}}",
|
|
7
|
+
artifactRepository: "cloud-run",
|
|
8
|
+
runtimeServiceAccount: "{{RUNTIME_SERVICE_ACCOUNT}}",
|
|
9
|
+
deployerServiceAccount: "{{DEPLOYER_SERVICE_ACCOUNT}}",
|
|
10
|
+
vaultRoleIdSecret: "{{VAULT_ROLE_ID_SECRET}}",
|
|
11
|
+
vaultSecretIdSecret: "{{VAULT_SECRET_ID_SECRET}}",
|
|
12
|
+
workloadIdentityPoolId: "{{WIF_POOL_ID}}",
|
|
13
|
+
workloadIdentityProviderId: "{{WIF_PROVIDER_ID}}",
|
|
14
|
+
requiredApis: [
|
|
15
|
+
"run.googleapis.com",
|
|
16
|
+
"cloudbuild.googleapis.com",
|
|
17
|
+
"artifactregistry.googleapis.com",
|
|
18
|
+
"iamcredentials.googleapis.com",
|
|
19
|
+
"sts.googleapis.com",
|
|
20
|
+
"secretmanager.googleapis.com",
|
|
21
|
+
"serviceusage.googleapis.com",
|
|
22
|
+
],
|
|
23
|
+
githubVariables: {
|
|
24
|
+
GCP_PROJECT_ID: "{{PROJECT_ID}}",
|
|
25
|
+
GCP_REGION: "{{REGION}}",
|
|
26
|
+
CLOUD_RUN_SERVICE: "{{SERVICE_NAME}}",
|
|
27
|
+
},
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const manifestEnv = {
|
|
31
|
+
SERVICE_NAME: "{{SERVICE_NAME}}",
|
|
32
|
+
RUNTIME_SERVICE_ACCOUNT: "{{RUNTIME_SERVICE_ACCOUNT}}",
|
|
33
|
+
VAULT_ADDR: "{{VAULT_ADDR}}",
|
|
34
|
+
VAULT_SECRET_PATH: "{{VAULT_SECRET_PATH}}",
|
|
35
|
+
VAULT_SECRET_KEY: "{{VAULT_SECRET_KEY}}",
|
|
36
|
+
CLOUDFLARE_ZONE_ID: "{{CLOUDFLARE_ZONE_ID}}",
|
|
37
|
+
VAULT_ROLE_ID_SECRET: "{{VAULT_ROLE_ID_SECRET}}",
|
|
38
|
+
VAULT_SECRET_ID_SECRET: "{{VAULT_SECRET_ID_SECRET}}",
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
export const bootstrapSecrets = [
|
|
42
|
+
{
|
|
43
|
+
secretName: "{{VAULT_ROLE_ID_SECRET}}",
|
|
44
|
+
bootstrapEnv: "BOOTSTRAP_VAULT_ROLE_ID",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
secretName: "{{VAULT_SECRET_ID_SECRET}}",
|
|
48
|
+
bootstrapEnv: "BOOTSTRAP_VAULT_SECRET_ID",
|
|
49
|
+
},
|
|
50
|
+
] as const;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { bootstrap } from "./bootstrap";
|
|
2
|
+
import { config } from "./config";
|
|
3
|
+
import { ensureArtifactRepository, gcloud, imageUrl, requireCommand, serviceUrl, writeRenderedManifest } from "./lib";
|
|
4
|
+
|
|
5
|
+
export async function deploy(args = Bun.argv.slice(2)) {
|
|
6
|
+
requireCommand("gcloud");
|
|
7
|
+
requireCommand("bun");
|
|
8
|
+
|
|
9
|
+
const ci = args.includes("--ci");
|
|
10
|
+
if (!ci) {
|
|
11
|
+
await bootstrap();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
ensureArtifactRepository();
|
|
15
|
+
|
|
16
|
+
const image = imageUrl();
|
|
17
|
+
gcloud(["builds", "submit", "--project", config.projectId, "--region", config.region, "--tag", image]);
|
|
18
|
+
|
|
19
|
+
const renderedManifestPath = await writeRenderedManifest(image);
|
|
20
|
+
gcloud(["run", "services", "replace", renderedManifestPath.pathname, "--project", config.projectId, "--region", config.region]);
|
|
21
|
+
gcloud([
|
|
22
|
+
"run",
|
|
23
|
+
"services",
|
|
24
|
+
"add-iam-policy-binding",
|
|
25
|
+
config.serviceName,
|
|
26
|
+
"--project",
|
|
27
|
+
config.projectId,
|
|
28
|
+
"--region",
|
|
29
|
+
config.region,
|
|
30
|
+
"--member",
|
|
31
|
+
"allUsers",
|
|
32
|
+
"--role",
|
|
33
|
+
"roles/run.invoker",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
console.log(serviceUrl());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (import.meta.main) {
|
|
40
|
+
await deploy();
|
|
41
|
+
}
|