feat: add devops-controlled dev ci flow
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -542,7 +542,7 @@ function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock,
|
||||
devMode ? h("div", { className: "dev-env-ribbon", "data-testid": "dev-environment-ribbon" },
|
||||
h("b", null, "DEV"),
|
||||
h("span", null, environment.namespace || "unidesk-dev"),
|
||||
h("span", null, environment.deployRef || "origin/deploy/dev"),
|
||||
h("span", null, environment.deployRef || "origin/master:deploy.json#environments.dev"),
|
||||
h("span", null, shortCommit(environment.commit || environment.requestedCommit)),
|
||||
) : null,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
ARG DEVOPS_GO_IMAGE=golang:1.23-bookworm
|
||||
ARG DEVOPS_RUNTIME_IMAGE=debian:bookworm-slim
|
||||
|
||||
FROM ${DEVOPS_GO_IMAGE} AS builder
|
||||
WORKDIR /src
|
||||
COPY src/components/microservices/devops/go.mod ./go.mod
|
||||
COPY src/components/microservices/devops/main.go ./main.go
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/unidesk-devops ./main.go
|
||||
|
||||
FROM ${DEVOPS_RUNTIME_IMAGE}
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /out/unidesk-devops /usr/local/bin/unidesk-devops
|
||||
|
||||
EXPOSE 4286
|
||||
CMD ["/usr/local/bin/unidesk-devops"]
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/pikasTech/unidesk/src/components/microservices/devops
|
||||
|
||||
go 1.23
|
||||
@@ -0,0 +1,739 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jsonMap map[string]any
|
||||
|
||||
type config struct {
|
||||
Host string
|
||||
Port int
|
||||
Namespace string
|
||||
RepoURL string
|
||||
DesiredRef string
|
||||
Environment string
|
||||
GitProxyURL string
|
||||
PipelineName string
|
||||
PipelineServiceAccount string
|
||||
WorkspaceClaim string
|
||||
AppImage string
|
||||
LogFile string
|
||||
}
|
||||
|
||||
type deployService struct {
|
||||
ID string `json:"id"`
|
||||
Repo string `json:"repo"`
|
||||
CommitID string `json:"commitId"`
|
||||
}
|
||||
|
||||
type deploySummary struct {
|
||||
DeployCommit string `json:"deployCommit"`
|
||||
DesiredRef string `json:"desiredRef"`
|
||||
Environment string `json:"environment"`
|
||||
RepoURL string `json:"repoUrl"`
|
||||
Services []deployService `json:"services"`
|
||||
}
|
||||
|
||||
type httpError struct {
|
||||
Status int
|
||||
Msg string
|
||||
Detail jsonMap
|
||||
}
|
||||
|
||||
func (e *httpError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
var (
|
||||
startedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
recentLogs []jsonMap
|
||||
serviceConfig = readConfig()
|
||||
refPattern = regexp.MustCompile(`^[A-Za-z0-9._/-]{1,160}$`)
|
||||
runIDPattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]{0,46}[a-z0-9])?$`)
|
||||
commitIDPattern = regexp.MustCompile(`^[0-9a-f]{7,40}$`)
|
||||
)
|
||||
|
||||
func envString(name, fallback string) string {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func envInt(name string, fallback int) int {
|
||||
raw := os.Getenv(name)
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func currentNamespace() string {
|
||||
raw, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
if err != nil {
|
||||
return "unidesk-ci"
|
||||
}
|
||||
value := strings.TrimSpace(string(raw))
|
||||
if value == "" {
|
||||
return "unidesk-ci"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func readConfig() config {
|
||||
return config{
|
||||
Host: envString("HOST", "0.0.0.0"),
|
||||
Port: envInt("PORT", 4286),
|
||||
Namespace: envString("DEVOPS_NAMESPACE", currentNamespace()),
|
||||
RepoURL: envString("DEVOPS_REPO_URL", "https://github.com/pikasTech/unidesk"),
|
||||
DesiredRef: envString("DEVOPS_DESIRED_REF", "master"),
|
||||
Environment: envString("DEVOPS_ENVIRONMENT", "dev"),
|
||||
GitProxyURL: envString("DEVOPS_GIT_PROXY_URL", "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"),
|
||||
PipelineName: envString("DEVOPS_DEV_E2E_PIPELINE", "unidesk-dev-namespace-e2e"),
|
||||
PipelineServiceAccount: envString("DEVOPS_PIPELINE_SERVICE_ACCOUNT", "unidesk-ci-runner"),
|
||||
WorkspaceClaim: envString("DEVOPS_PIPELINE_WORKSPACE_CLAIM", "unidesk-ci-cache"),
|
||||
AppImage: envString("DEVOPS_DEV_E2E_APP_IMAGE", "unidesk-code-queue:dev"),
|
||||
LogFile: envString("LOG_FILE", "/var/log/unidesk/devops.jsonl"),
|
||||
}
|
||||
}
|
||||
|
||||
func appendLog(level, event string, detail jsonMap) {
|
||||
record := jsonMap{"at": time.Now().UTC().Format(time.RFC3339), "service": "devops", "level": level, "event": event}
|
||||
for key, value := range detail {
|
||||
record[key] = value
|
||||
}
|
||||
recentLogs = append(recentLogs, record)
|
||||
if len(recentLogs) > 300 {
|
||||
recentLogs = recentLogs[len(recentLogs)-300:]
|
||||
}
|
||||
line, _ := json.Marshal(record)
|
||||
log.Println(string(line))
|
||||
if serviceConfig.LogFile != "" {
|
||||
_ = os.MkdirAll(filepath.Dir(serviceConfig.LogFile), 0o755)
|
||||
if file, err := os.OpenFile(serviceConfig.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644); err == nil {
|
||||
_, _ = file.Write(append(line, '\n'))
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("content-type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func errorBody(err error) jsonMap {
|
||||
body := jsonMap{"ok": false, "error": err.Error()}
|
||||
var he *httpError
|
||||
if errors.As(err, &he) {
|
||||
for key, value := range he.Detail {
|
||||
body[key] = value
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func handleError(w http.ResponseWriter, err error) {
|
||||
status := http.StatusInternalServerError
|
||||
var he *httpError
|
||||
if errors.As(err, &he) {
|
||||
status = he.Status
|
||||
}
|
||||
appendLog(map[bool]string{true: "error", false: "warn"}[status >= 500], "request_failed", jsonMap{"status": status, "error": err.Error()})
|
||||
writeJSON(w, status, errorBody(err))
|
||||
}
|
||||
|
||||
func readJSON(r *http.Request) (jsonMap, error) {
|
||||
if r.Body == nil {
|
||||
return jsonMap{}, nil
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(strings.TrimSpace(string(body))) == 0 {
|
||||
return jsonMap{}, nil
|
||||
}
|
||||
var record jsonMap
|
||||
if err := json.Unmarshal(body, &record); err != nil {
|
||||
return nil, &httpError{Status: http.StatusBadRequest, Msg: "request body must be JSON"}
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
text, _ := value.(string)
|
||||
return text
|
||||
}
|
||||
|
||||
func boolValue(value any) bool {
|
||||
switch item := value.(type) {
|
||||
case bool:
|
||||
return item
|
||||
case string:
|
||||
return item == "true" || item == "1"
|
||||
case float64:
|
||||
return item == 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func requireRepoURL(value any) (string, error) {
|
||||
repo := stringValue(value)
|
||||
if repo == "" {
|
||||
repo = serviceConfig.RepoURL
|
||||
}
|
||||
parsed, err := url.Parse(repo)
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
|
||||
return "", &httpError{Status: http.StatusBadRequest, Msg: "repoUrl must be an https URL"}
|
||||
}
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func requireDesiredRef(value any) (string, error) {
|
||||
ref := stringValue(value)
|
||||
if ref == "" {
|
||||
ref = serviceConfig.DesiredRef
|
||||
}
|
||||
if !refPattern.MatchString(ref) || strings.HasPrefix(ref, "-") || strings.Contains(ref, "..") {
|
||||
return "", &httpError{Status: http.StatusBadRequest, Msg: "desired ref contains unsupported characters"}
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func optionalRunID(value any) (string, error) {
|
||||
runID := stringValue(value)
|
||||
if runID == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !runIDPattern.MatchString(runID) {
|
||||
return "", &httpError{Status: http.StatusBadRequest, Msg: "runId must be DNS-safe lowercase alnum/dash, max 48 chars"}
|
||||
}
|
||||
return runID, nil
|
||||
}
|
||||
|
||||
func gitEnv() []string {
|
||||
env := os.Environ()
|
||||
noProxy := "localhost,127.0.0.1,::1,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local"
|
||||
add := map[string]string{
|
||||
"HTTP_PROXY": serviceConfig.GitProxyURL,
|
||||
"HTTPS_PROXY": serviceConfig.GitProxyURL,
|
||||
"ALL_PROXY": serviceConfig.GitProxyURL,
|
||||
"NO_PROXY": noProxy,
|
||||
"http_proxy": serviceConfig.GitProxyURL,
|
||||
"https_proxy": serviceConfig.GitProxyURL,
|
||||
"all_proxy": serviceConfig.GitProxyURL,
|
||||
"no_proxy": noProxy,
|
||||
}
|
||||
for key, value := range add {
|
||||
env = append(env, key+"="+value)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func runCommand(ctx context.Context, cwd string, args ...string) (string, string, error) {
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
cmd.Dir = cwd
|
||||
cmd.Env = gitEnv()
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
return stdout.String(), stderr.String(), err
|
||||
}
|
||||
|
||||
func resolveDeployManifest(repoURL, desiredRef, environment string) (deploySummary, error) {
|
||||
dir, err := os.MkdirTemp("", "unidesk-devops-deploy-")
|
||||
if err != nil {
|
||||
return deploySummary{}, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Second)
|
||||
defer cancel()
|
||||
if _, stderr, err := runCommand(ctx, dir, "git", "init", "-q"); err != nil {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadGateway, Msg: "git init failed", Detail: jsonMap{"stderr": tail(stderr, 2000)}}
|
||||
}
|
||||
if _, stderr, err := runCommand(ctx, dir, "git", "remote", "add", "origin", repoURL); err != nil {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadGateway, Msg: "git remote add failed", Detail: jsonMap{"stderr": tail(stderr, 2000)}}
|
||||
}
|
||||
if _, stderr, err := runCommand(ctx, dir, "git", "fetch", "--depth=1", "origin", desiredRef); err != nil {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadGateway, Msg: "failed to fetch desired ref", Detail: jsonMap{"stderr": tail(stderr, 4000)}}
|
||||
}
|
||||
stdout, stderr, err := runCommand(ctx, dir, "git", "rev-parse", "FETCH_HEAD")
|
||||
if err != nil {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadGateway, Msg: "failed to resolve desired ref commit", Detail: jsonMap{"stderr": tail(stderr, 2000)}}
|
||||
}
|
||||
deployCommit := strings.TrimSpace(stdout)
|
||||
stdout, stderr, err = runCommand(ctx, dir, "git", "show", "FETCH_HEAD:deploy.json")
|
||||
if err != nil {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadGateway, Msg: "failed to read deploy.json from desired ref", Detail: jsonMap{"stderr": tail(stderr, 4000)}}
|
||||
}
|
||||
return parseDeployManifest(stdout, repoURL, desiredRef, environment, deployCommit)
|
||||
}
|
||||
|
||||
func parseDeployManifest(raw, repoURL, desiredRef, environment, deployCommit string) (deploySummary, error) {
|
||||
var parsed struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Environments map[string]struct {
|
||||
Services []deployService `json:"services"`
|
||||
} `json:"environments"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadRequest, Msg: "deploy.json must be valid JSON"}
|
||||
}
|
||||
if parsed.SchemaVersion != 2 {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadRequest, Msg: "deploy.json must use schemaVersion=2"}
|
||||
}
|
||||
env, ok := parsed.Environments[environment]
|
||||
if !ok {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadRequest, Msg: "deploy.json must contain requested environment", Detail: jsonMap{"environment": environment}}
|
||||
}
|
||||
if len(env.Services) == 0 {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadRequest, Msg: "deploy.json environment must contain services", Detail: jsonMap{"environment": environment}}
|
||||
}
|
||||
for index, service := range env.Services {
|
||||
service.CommitID = strings.ToLower(service.CommitID)
|
||||
env.Services[index].CommitID = service.CommitID
|
||||
if service.ID == "" || service.Repo == "" || !commitIDPattern.MatchString(service.CommitID) {
|
||||
return deploySummary{}, &httpError{Status: http.StatusBadRequest, Msg: fmt.Sprintf("deploy.json environments.%s.services[%d] must contain id, repo and 7-40 char commitId", environment, index)}
|
||||
}
|
||||
}
|
||||
return deploySummary{DeployCommit: deployCommit, DesiredRef: desiredRef, Environment: environment, RepoURL: repoURL, Services: env.Services}, nil
|
||||
}
|
||||
|
||||
func tail(value string, max int) string {
|
||||
if len(value) <= max {
|
||||
return value
|
||||
}
|
||||
return value[len(value)-max:]
|
||||
}
|
||||
|
||||
func randomSuffix() string {
|
||||
var bytes [4]byte
|
||||
if _, err := rand.Read(bytes[:]); err != nil {
|
||||
return strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||
}
|
||||
return hex.EncodeToString(bytes[:])
|
||||
}
|
||||
|
||||
func makeRunID(deployCommit string) string {
|
||||
stamp := time.Now().UTC().Format("20060102150405")
|
||||
runID := fmt.Sprintf("%s-%s", stamp, strings.ToLower(deployCommit[:min(len(deployCommit), 8)]))
|
||||
runID = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(runID, "-")
|
||||
if len(runID) > 48 {
|
||||
return runID[:48]
|
||||
}
|
||||
return runID
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func pipelineRunBody(summary deploySummary, runID string, keepNamespace bool) jsonMap {
|
||||
return jsonMap{
|
||||
"apiVersion": "tekton.dev/v1",
|
||||
"kind": "PipelineRun",
|
||||
"metadata": jsonMap{
|
||||
"generateName": fmt.Sprintf("unidesk-dev-e2e-%s-", runID),
|
||||
"namespace": serviceConfig.Namespace,
|
||||
"labels": jsonMap{
|
||||
"app.kubernetes.io/name": "unidesk-dev-namespace-e2e",
|
||||
"app.kubernetes.io/part-of": "unidesk",
|
||||
"unidesk.ai/ci-kind": "dev-namespace-e2e",
|
||||
"unidesk.ai/deploy-ref": "master-deploy-json-dev",
|
||||
"unidesk.ai/deploy-commit": summary.DeployCommit[:min(len(summary.DeployCommit), 40)],
|
||||
},
|
||||
},
|
||||
"spec": jsonMap{
|
||||
"pipelineRef": jsonMap{"name": serviceConfig.PipelineName},
|
||||
"taskRunTemplate": jsonMap{"serviceAccountName": serviceConfig.PipelineServiceAccount},
|
||||
"params": []jsonMap{
|
||||
{"name": "repo-url", "value": summary.RepoURL},
|
||||
{"name": "desired-ref", "value": summary.DesiredRef},
|
||||
{"name": "deploy-commit", "value": summary.DeployCommit},
|
||||
{"name": "environment", "value": summary.Environment},
|
||||
{"name": "run-id", "value": runID},
|
||||
{"name": "keep-namespace", "value": map[bool]string{true: "true", false: "false"}[keepNamespace]},
|
||||
{"name": "app-image", "value": serviceConfig.AppImage},
|
||||
},
|
||||
"workspaces": []jsonMap{
|
||||
{"name": "shared-workspace", "persistentVolumeClaim": jsonMap{"claimName": serviceConfig.WorkspaceClaim}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func serviceAccountFile(name string) string {
|
||||
return filepath.Join("/var/run/secrets/kubernetes.io/serviceaccount", name)
|
||||
}
|
||||
|
||||
func kubeClient() (*http.Client, error) {
|
||||
caPEM, err := os.ReadFile(serviceAccountFile("ca.crt"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caPEM) {
|
||||
return nil, errors.New("failed to load service-account CA")
|
||||
}
|
||||
return &http.Client{Timeout: 120 * time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}}}, nil
|
||||
}
|
||||
|
||||
func kubeURL(path string) string {
|
||||
host := envString("KUBERNETES_SERVICE_HOST", "kubernetes.default.svc")
|
||||
port := envString("KUBERNETES_SERVICE_PORT_HTTPS", envString("KUBERNETES_SERVICE_PORT", "443"))
|
||||
return fmt.Sprintf("https://%s:%s%s", host, port, path)
|
||||
}
|
||||
|
||||
func kubeRequest(method, path string, body any) (int, []byte, error) {
|
||||
client, err := kubeClient()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
reader = bytes.NewReader(raw)
|
||||
}
|
||||
req, err := http.NewRequest(method, kubeURL(path), reader)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
token, err := os.ReadFile(serviceAccountFile("token"))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
req.Header.Set("authorization", "Bearer "+strings.TrimSpace(string(token)))
|
||||
req.Header.Set("accept", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("content-type", "application/json")
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(res.Body, 8*1024*1024))
|
||||
if err != nil {
|
||||
return res.StatusCode, nil, err
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
return res.StatusCode, raw, &httpError{Status: res.StatusCode, Msg: "kubernetes api request failed", Detail: jsonMap{"path": path, "status": res.StatusCode, "body": tail(string(raw), 4000)}}
|
||||
}
|
||||
return res.StatusCode, raw, nil
|
||||
}
|
||||
|
||||
func metadataName(value any) string {
|
||||
record, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
metadata, ok := record["metadata"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return stringValue(metadata["name"])
|
||||
}
|
||||
|
||||
func conditionSummary(value any) jsonMap {
|
||||
record, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return jsonMap{"terminal": false, "status": "Unknown", "reason": "", "message": ""}
|
||||
}
|
||||
status, _ := record["status"].(map[string]any)
|
||||
conditions, _ := status["conditions"].([]any)
|
||||
for _, item := range conditions {
|
||||
condition, ok := item.(map[string]any)
|
||||
if !ok || condition["type"] != "Succeeded" {
|
||||
continue
|
||||
}
|
||||
statusText := stringValue(condition["status"])
|
||||
return jsonMap{
|
||||
"terminal": statusText == "True" || statusText == "False",
|
||||
"succeeded": statusText == "True",
|
||||
"status": statusText,
|
||||
"reason": stringValue(condition["reason"]),
|
||||
"message": tail(stringValue(condition["message"]), 2000),
|
||||
}
|
||||
}
|
||||
return jsonMap{"terminal": false, "status": "Unknown", "reason": "", "message": ""}
|
||||
}
|
||||
|
||||
func decodeJSON(raw []byte) any {
|
||||
var value any
|
||||
if err := json.Unmarshal(raw, &value); err != nil {
|
||||
return jsonMap{"text": tail(string(raw), 4000)}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func listItems(value any) []any {
|
||||
record, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
items, _ := record["items"].([]any)
|
||||
return items
|
||||
}
|
||||
|
||||
func compactTaskRun(value any) jsonMap {
|
||||
record, _ := value.(map[string]any)
|
||||
status, _ := record["status"].(map[string]any)
|
||||
return jsonMap{
|
||||
"name": metadataName(value),
|
||||
"condition": conditionSummary(value),
|
||||
"podName": stringValue(status["podName"]),
|
||||
"startTime": stringValue(status["startTime"]),
|
||||
"completionTime": stringValue(status["completionTime"]),
|
||||
}
|
||||
}
|
||||
|
||||
func compactPod(value any) jsonMap {
|
||||
record, _ := value.(map[string]any)
|
||||
status, _ := record["status"].(map[string]any)
|
||||
spec, _ := record["spec"].(map[string]any)
|
||||
return jsonMap{
|
||||
"name": metadataName(value),
|
||||
"phase": stringValue(status["phase"]),
|
||||
"nodeName": stringValue(spec["nodeName"]),
|
||||
"podIP": stringValue(status["podIP"]),
|
||||
"reason": stringValue(status["reason"]),
|
||||
}
|
||||
}
|
||||
|
||||
func runStatus(name string) (jsonMap, error) {
|
||||
namespace := url.PathEscape(serviceConfig.Namespace)
|
||||
selector := url.QueryEscape("tekton.dev/pipelineRun=" + name)
|
||||
_, pipelineRaw, err := kubeRequest("GET", fmt.Sprintf("/apis/tekton.dev/v1/namespaces/%s/pipelineruns/%s", namespace, url.PathEscape(name)), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, taskRaw, _ := kubeRequest("GET", fmt.Sprintf("/apis/tekton.dev/v1/namespaces/%s/taskruns?labelSelector=%s", namespace, selector), nil)
|
||||
_, podRaw, _ := kubeRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/pods?labelSelector=%s", namespace, selector), nil)
|
||||
taskRuns := []jsonMap{}
|
||||
for _, item := range listItems(decodeJSON(taskRaw)) {
|
||||
taskRuns = append(taskRuns, compactTaskRun(item))
|
||||
}
|
||||
pods := []jsonMap{}
|
||||
for _, item := range listItems(decodeJSON(podRaw)) {
|
||||
pods = append(pods, compactPod(item))
|
||||
}
|
||||
pipeline := decodeJSON(pipelineRaw)
|
||||
return jsonMap{"ok": true, "pipelineRun": name, "namespace": serviceConfig.Namespace, "condition": conditionSummary(pipeline), "taskRuns": taskRuns, "pods": pods}, nil
|
||||
}
|
||||
|
||||
func handleRunDevE2E(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := readJSON(r)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
repoURL, err := requireRepoURL(body["repoUrl"])
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
desiredRef, err := requireDesiredRef(body["desiredRef"])
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
environment := stringValue(body["environment"])
|
||||
if environment == "" {
|
||||
environment = serviceConfig.Environment
|
||||
}
|
||||
if environment != "dev" {
|
||||
handleError(w, &httpError{Status: http.StatusBadRequest, Msg: "only environment=dev is enabled for dev e2e"})
|
||||
return
|
||||
}
|
||||
runID, err := optionalRunID(body["runId"])
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
keepNamespace := boolValue(body["keepNamespace"])
|
||||
summary, err := resolveDeployManifest(repoURL, desiredRef, environment)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
if runID == "" {
|
||||
runID = makeRunID(summary.DeployCommit)
|
||||
}
|
||||
namespace := url.PathEscape(serviceConfig.Namespace)
|
||||
_, raw, err := kubeRequest("POST", fmt.Sprintf("/apis/tekton.dev/v1/namespaces/%s/pipelineruns", namespace), pipelineRunBody(summary, runID, keepNamespace))
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
name := metadataName(decodeJSON(raw))
|
||||
if name == "" {
|
||||
name = "unknown-" + randomSuffix()
|
||||
}
|
||||
appendLog("info", "dev_e2e_started", jsonMap{"pipelineRun": name, "runId": runID, "deployCommit": summary.DeployCommit, "desiredRef": desiredRef, "environment": environment, "keepNamespace": keepNamespace})
|
||||
writeJSON(w, http.StatusOK, jsonMap{
|
||||
"ok": true,
|
||||
"mode": "k3s-devops-managed",
|
||||
"pipelineRun": name,
|
||||
"namespace": serviceConfig.Namespace,
|
||||
"temporaryNamespace": "unidesk-ci-e2e-" + runID,
|
||||
"runId": runID,
|
||||
"keepNamespace": keepNamespace,
|
||||
"desiredRef": desiredRef,
|
||||
"environment": environment,
|
||||
"deployCommit": summary.DeployCommit,
|
||||
"services": summary.Services,
|
||||
"next": []string{"bun scripts/cli.ts ci logs " + name, "bun scripts/cli.ts ci status"},
|
||||
})
|
||||
}
|
||||
|
||||
func handleLogs(w http.ResponseWriter, name string) {
|
||||
namespace := url.PathEscape(serviceConfig.Namespace)
|
||||
selector := url.QueryEscape("tekton.dev/pipelineRun=" + name)
|
||||
_, podRaw, err := kubeRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/pods?labelSelector=%s", namespace, selector), nil)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
logs := []jsonMap{}
|
||||
for index, item := range listItems(decodeJSON(podRaw)) {
|
||||
if index >= 12 {
|
||||
break
|
||||
}
|
||||
podName := metadataName(item)
|
||||
if podName == "" {
|
||||
continue
|
||||
}
|
||||
_, raw, err := kubeRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log?allContainers=true&tailLines=180", namespace, url.PathEscape(podName)), nil)
|
||||
if err != nil {
|
||||
logs = append(logs, jsonMap{"pod": podName, "ok": false, "error": err.Error()})
|
||||
} else {
|
||||
logs = append(logs, jsonMap{"pod": podName, "ok": true, "text": tail(string(raw), 40000)})
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, jsonMap{"ok": true, "pipelineRun": name, "namespace": serviceConfig.Namespace, "logs": logs})
|
||||
}
|
||||
|
||||
func handleCIStatus(w http.ResponseWriter) {
|
||||
namespace := url.PathEscape(serviceConfig.Namespace)
|
||||
_, pipelinesRaw, _ := kubeRequest("GET", fmt.Sprintf("/apis/tekton.dev/v1/namespaces/%s/pipelines", namespace), nil)
|
||||
_, tasksRaw, _ := kubeRequest("GET", fmt.Sprintf("/apis/tekton.dev/v1/namespaces/%s/tasks", namespace), nil)
|
||||
_, runsRaw, _ := kubeRequest("GET", fmt.Sprintf("/apis/tekton.dev/v1/namespaces/%s/pipelineruns", namespace), nil)
|
||||
writeJSON(w, http.StatusOK, jsonMap{
|
||||
"ok": true,
|
||||
"service": "devops",
|
||||
"namespace": serviceConfig.Namespace,
|
||||
"mode": "k3s-devops-managed",
|
||||
"pipelines": namesFromItems(decodeJSON(pipelinesRaw)),
|
||||
"tasks": namesFromItems(decodeJSON(tasksRaw)),
|
||||
"recentPipelineRuns": namesFromItems(decodeJSON(runsRaw)),
|
||||
})
|
||||
}
|
||||
|
||||
func namesFromItems(value any) []string {
|
||||
names := []string{}
|
||||
for _, item := range listItems(value) {
|
||||
if name := metadataName(item); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
if len(names) > 20 {
|
||||
return names[len(names)-20:]
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func router(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodOptions {
|
||||
writeJSON(w, http.StatusOK, jsonMap{"ok": true})
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case (r.URL.Path == "/" || r.URL.Path == "/health") && (r.Method == http.MethodGet || r.Method == http.MethodHead):
|
||||
if r.Method == http.MethodHead {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, jsonMap{
|
||||
"ok": true,
|
||||
"service": "devops",
|
||||
"startedAt": startedAt,
|
||||
"namespace": serviceConfig.Namespace,
|
||||
"mode": "k3s-devops-managed",
|
||||
"normalControlPlane": "CLI -> backend-core -> k3sctl-adapter -> devops -> Kubernetes API/Tekton",
|
||||
"breakGlass": "provider-gateway host.ssh remains bootstrap/recovery only",
|
||||
})
|
||||
case r.URL.Path == "/live" && (r.Method == http.MethodGet || r.Method == http.MethodHead):
|
||||
if r.Method == http.MethodHead {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, jsonMap{"ok": true, "service": "devops", "startedAt": startedAt})
|
||||
case r.URL.Path == "/logs" && r.Method == http.MethodGet:
|
||||
writeJSON(w, http.StatusOK, jsonMap{"ok": true, "logs": recentLogs})
|
||||
case r.URL.Path == "/api/ci/status" && r.Method == http.MethodGet:
|
||||
handleCIStatus(w)
|
||||
case r.URL.Path == "/api/ci/dev-e2e/run" && r.Method == http.MethodPost:
|
||||
handleRunDevE2E(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ci/runs/") && strings.HasSuffix(r.URL.Path, "/logs") && r.Method == http.MethodGet:
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/api/ci/runs/"), "/logs")
|
||||
handleLogs(w, name)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ci/runs/") && r.Method == http.MethodGet:
|
||||
name := strings.TrimPrefix(r.URL.Path, "/api/ci/runs/")
|
||||
status, err := runStatus(name)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, jsonMap{"ok": false, "error": "not found"})
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
appendLog("info", "service_started", jsonMap{"port": serviceConfig.Port, "namespace": serviceConfig.Namespace, "pipelineName": serviceConfig.PipelineName, "appImage": serviceConfig.AppImage})
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", serviceConfig.Host, serviceConfig.Port),
|
||||
Handler: http.HandlerFunc(router),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
@@ -40,7 +40,8 @@ services:
|
||||
K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE:-}"
|
||||
K3SCTL_NATIVE_SERVICE_URL_MDTODO: "${K3SCTL_NATIVE_SERVICE_URL_MDTODO:-}"
|
||||
K3SCTL_NATIVE_SERVICE_URL_DECISION_CENTER: "${K3SCTL_NATIVE_SERVICE_URL_DECISION_CENTER:-}"
|
||||
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json}"
|
||||
K3SCTL_NATIVE_SERVICE_URL_DEVOPS: "${K3SCTL_NATIVE_SERVICE_URL_DEVOPS:-}"
|
||||
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json,k3s/devops.k3s.json}"
|
||||
K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}"
|
||||
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
|
||||
volumes:
|
||||
|
||||
@@ -77,6 +77,40 @@ roleRef:
|
||||
name: unidesk-ci-trigger-reader
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: unidesk-ci-dev-e2e-manager
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-ci
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "watch", "create", "delete", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps", "services", "pods", "pods/log"]
|
||||
verbs: ["get", "list", "watch", "create", "delete", "patch"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "list", "watch", "create", "delete", "patch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: unidesk-ci-dev-e2e-manager
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-ci
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: unidesk-ci-runner
|
||||
namespace: unidesk-ci
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: unidesk-ci-dev-e2e-manager
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: unidesk-ci-cross-namespace
|
||||
@@ -146,7 +180,7 @@ spec:
|
||||
type: string
|
||||
- name: image
|
||||
type: string
|
||||
default: unidesk-code-queue:d601
|
||||
default: unidesk-code-queue:dev
|
||||
workspaces:
|
||||
- name: source
|
||||
volumes:
|
||||
@@ -185,6 +219,7 @@ spec:
|
||||
git rev-parse HEAD | tee "$(workspaces.source.path)/commit.txt"
|
||||
- name: install-and-check
|
||||
image: "$(params.image)"
|
||||
imagePullPolicy: Never
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: unix:///var/run/docker.sock
|
||||
@@ -235,12 +270,13 @@ spec:
|
||||
type: string
|
||||
- name: app-image
|
||||
type: string
|
||||
default: unidesk-code-queue:d601
|
||||
default: unidesk-code-queue:dev
|
||||
workspaces:
|
||||
- name: source
|
||||
steps:
|
||||
- name: start-read-service
|
||||
image: "$(params.app-image)"
|
||||
imagePullPolicy: Never
|
||||
env:
|
||||
- name: HTTP_PROXY
|
||||
value: ""
|
||||
@@ -499,6 +535,7 @@ spec:
|
||||
exit 1
|
||||
- name: measure
|
||||
image: "$(params.app-image)"
|
||||
imagePullPolicy: Never
|
||||
workingDir: "$(workspaces.source.path)/repo"
|
||||
env:
|
||||
- name: CI_CODE_QUEUE_URL
|
||||
@@ -550,6 +587,7 @@ spec:
|
||||
bun scripts/ci-code-queue-read-perf.ts
|
||||
- name: cleanup
|
||||
image: "$(params.app-image)"
|
||||
imagePullPolicy: Never
|
||||
env:
|
||||
- name: HTTP_PROXY
|
||||
value: ""
|
||||
@@ -588,6 +626,408 @@ spec:
|
||||
delete_resource "api/v1/namespaces/$kube_namespace/services/code-queue-ci-read"
|
||||
---
|
||||
apiVersion: tekton.dev/v1
|
||||
kind: Task
|
||||
metadata:
|
||||
name: unidesk-dev-namespace-e2e
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-ci
|
||||
app.kubernetes.io/component: dev-namespace-e2e
|
||||
spec:
|
||||
params:
|
||||
- name: repo-url
|
||||
type: string
|
||||
default: https://github.com/pikasTech/unidesk
|
||||
- name: desired-ref
|
||||
type: string
|
||||
default: master
|
||||
- name: deploy-commit
|
||||
type: string
|
||||
- name: environment
|
||||
type: string
|
||||
default: dev
|
||||
- name: run-id
|
||||
type: string
|
||||
- name: keep-namespace
|
||||
type: string
|
||||
default: "false"
|
||||
- name: app-image
|
||||
type: string
|
||||
default: unidesk-code-queue:dev
|
||||
workspaces:
|
||||
- name: source
|
||||
steps:
|
||||
- name: clone-deploy-manifest
|
||||
image: alpine/git:2.45.2
|
||||
env:
|
||||
- name: HTTP_PROXY
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: HTTPS_PROXY
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: ALL_PROXY
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: NO_PROXY
|
||||
value: "localhost,127.0.0.1,::1,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local"
|
||||
- name: http_proxy
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: https_proxy
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: all_proxy
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: no_proxy
|
||||
value: "localhost,127.0.0.1,::1,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local"
|
||||
script: |
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
run_dir="$(workspaces.source.path)/dev-e2e-$(params.run-id)"
|
||||
rm -rf "$run_dir"
|
||||
mkdir -p "$run_dir"
|
||||
git clone --filter=blob:none --no-checkout "$(params.repo-url)" "$run_dir/repo"
|
||||
cd "$run_dir/repo"
|
||||
git fetch --depth=1 origin "$(params.desired-ref)"
|
||||
actual="$(git rev-parse FETCH_HEAD)"
|
||||
expected="$(params.deploy-commit)"
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "desired ref commit mismatch actual=$actual expected=$expected" >&2
|
||||
exit 1
|
||||
fi
|
||||
git show FETCH_HEAD:deploy.json >"$run_dir/deploy.json"
|
||||
printf '%s\n' "$actual" >"$run_dir/deploy-commit.txt"
|
||||
- name: namespace-smoke-e2e
|
||||
image: "$(params.app-image)"
|
||||
imagePullPolicy: Never
|
||||
env:
|
||||
- name: HTTP_PROXY
|
||||
value: ""
|
||||
- name: HTTPS_PROXY
|
||||
value: ""
|
||||
- name: ALL_PROXY
|
||||
value: ""
|
||||
- name: NO_PROXY
|
||||
value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local"
|
||||
- name: http_proxy
|
||||
value: ""
|
||||
- name: https_proxy
|
||||
value: ""
|
||||
- name: all_proxy
|
||||
value: ""
|
||||
- name: no_proxy
|
||||
value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local"
|
||||
script: |
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ns="unidesk-ci-e2e-$(params.run-id)"
|
||||
keep="$(params.keep-namespace)"
|
||||
run_dir="$(workspaces.source.path)/dev-e2e-$(params.run-id)"
|
||||
deploy_json="$run_dir/deploy.json"
|
||||
result_json="$run_dir/dev-e2e-result.json"
|
||||
kube_api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}"
|
||||
kube_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
|
||||
kube_ca="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
kube() {
|
||||
local method="$1"
|
||||
shift
|
||||
curl -fsS --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X "$method" "$@"
|
||||
}
|
||||
delete_path() {
|
||||
local path="$1"
|
||||
local code
|
||||
code="$(curl -sS -o /tmp/unidesk-dev-e2e-delete-response -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X DELETE "$kube_api/$path")"
|
||||
if [ "$code" = "200" ] || [ "$code" = "202" ] || [ "$code" = "404" ]; then
|
||||
return 0
|
||||
fi
|
||||
cat /tmp/unidesk-dev-e2e-delete-response >&2
|
||||
return 1
|
||||
}
|
||||
cleanup() {
|
||||
if [ "$keep" = "true" ]; then
|
||||
echo "dev_e2e_namespace_retained=$ns"
|
||||
return 0
|
||||
fi
|
||||
delete_path "api/v1/namespaces/$ns" || true
|
||||
echo "dev_e2e_namespace_deleted=$ns"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
bun - "$deploy_json" "$run_dir/dev-e2e-service-commits.env" "$(params.environment)" <<'BUN'
|
||||
const [deployPath, outPath, environment] = process.argv.slice(2);
|
||||
const manifest = await Bun.file(deployPath).json();
|
||||
if (!manifest || manifest.schemaVersion !== 2 || !manifest.environments?.[environment]) {
|
||||
throw new Error(`deploy.json must contain schemaVersion=2 environments.${environment}`);
|
||||
}
|
||||
const services = manifest.environments[environment].services;
|
||||
if (!Array.isArray(services) || services.length === 0) {
|
||||
throw new Error(`deploy.json environments.${environment}.services must contain services`);
|
||||
}
|
||||
const lines = [];
|
||||
for (const service of services) {
|
||||
if (!service?.id || !service?.commitId || !service?.repo) {
|
||||
throw new Error(`each deploy.json environments.${environment} service must contain id, repo and commitId`);
|
||||
}
|
||||
const key = String(service.id).toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
||||
lines.push(`${key}_COMMIT=${service.commitId}`);
|
||||
}
|
||||
await Bun.write(outPath, lines.join("\n") + "\n");
|
||||
console.log(JSON.stringify({ ok: true, environment, services: services.map((service) => ({ id: service.id, commitId: service.commitId })) }));
|
||||
BUN
|
||||
# shellcheck disable=SC1091
|
||||
source "$run_dir/dev-e2e-service-commits.env"
|
||||
backend_commit="${BACKEND_CORE_COMMIT:-unknown}"
|
||||
frontend_commit="${FRONTEND_COMMIT:-unknown}"
|
||||
code_queue_commit="${CODE_QUEUE_COMMIT:-unknown}"
|
||||
|
||||
delete_path "api/v1/namespaces/$ns"
|
||||
cat >/tmp/dev-e2e-namespace.yaml <<YAML
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: $ns
|
||||
labels:
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/purpose: ci-dev-e2e
|
||||
unidesk.ai/deploy-ref: master-deploy-json-dev
|
||||
YAML
|
||||
kube PATCH \
|
||||
-H "Content-Type: application/apply-patch+yaml" \
|
||||
--data-binary @/tmp/dev-e2e-namespace.yaml \
|
||||
"$kube_api/api/v1/namespaces/$ns?fieldManager=unidesk-ci&force=true" >/dev/null
|
||||
|
||||
deploy_json_b64="$(base64 -w0 "$deploy_json")"
|
||||
cat >/tmp/dev-e2e-configmap.yaml <<YAML
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: desired-manifest
|
||||
namespace: $ns
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-dev-namespace-e2e
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
data:
|
||||
deployCommit: "$(params.deploy-commit)"
|
||||
environment: "$(params.environment)"
|
||||
backendCoreCommit: "$backend_commit"
|
||||
frontendCommit: "$frontend_commit"
|
||||
codeQueueCommit: "$code_queue_commit"
|
||||
deploy.json.b64: "$deploy_json_b64"
|
||||
YAML
|
||||
kube PATCH \
|
||||
-H "Content-Type: application/apply-patch+yaml" \
|
||||
--data-binary @/tmp/dev-e2e-configmap.yaml \
|
||||
"$kube_api/api/v1/namespaces/$ns/configmaps/desired-manifest?fieldManager=unidesk-ci&force=true" >/dev/null
|
||||
|
||||
cat >/tmp/dev-e2e-target.yaml <<YAML
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dev-e2e-target
|
||||
namespace: $ns
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-dev-namespace-e2e
|
||||
app.kubernetes.io/component: smoke-target
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: unidesk-dev-namespace-e2e
|
||||
app.kubernetes.io/component: smoke-target
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-dev-namespace-e2e
|
||||
app.kubernetes.io/component: smoke-target
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
spec:
|
||||
nodeSelector:
|
||||
unidesk.ai/node-id: D601
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: smoke-target
|
||||
image: "$(params.app-image)"
|
||||
imagePullPolicy: Never
|
||||
command:
|
||||
- bun
|
||||
- -e
|
||||
- |
|
||||
const port = Number(process.env.PORT || 8080);
|
||||
const payload = {
|
||||
ok: true,
|
||||
environment: "dev",
|
||||
namespace: process.env.CI_E2E_NAMESPACE,
|
||||
deployCommit: process.env.CI_E2E_DEPLOY_COMMIT,
|
||||
backendCoreCommit: process.env.BACKEND_CORE_COMMIT,
|
||||
frontendCommit: process.env.FRONTEND_COMMIT,
|
||||
codeQueueCommit: process.env.CODE_QUEUE_COMMIT
|
||||
};
|
||||
Bun.serve({
|
||||
hostname: "0.0.0.0",
|
||||
port,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/health" || url.pathname === "/") {
|
||||
return Response.json(payload);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
});
|
||||
console.log(JSON.stringify({ listening: port, ...payload }));
|
||||
await new Promise(() => {});
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
- name: CI_E2E_NAMESPACE
|
||||
value: "$ns"
|
||||
- name: CI_E2E_DEPLOY_COMMIT
|
||||
value: "$(params.deploy-commit)"
|
||||
- name: BACKEND_CORE_COMMIT
|
||||
value: "$backend_commit"
|
||||
- name: FRONTEND_COMMIT
|
||||
value: "$frontend_commit"
|
||||
- name: CODE_QUEUE_COMMIT
|
||||
value: "$code_queue_commit"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 3
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 20
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dev-e2e-target
|
||||
namespace: $ns
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-dev-namespace-e2e
|
||||
app.kubernetes.io/component: smoke-target
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: unidesk-dev-namespace-e2e
|
||||
app.kubernetes.io/component: smoke-target
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
YAML
|
||||
csplit -s -f /tmp/dev-e2e-target- /tmp/dev-e2e-target.yaml '/^---$/' '{*}'
|
||||
kube PATCH \
|
||||
-H "Content-Type: application/apply-patch+yaml" \
|
||||
--data-binary @/tmp/dev-e2e-target-00 \
|
||||
"$kube_api/apis/apps/v1/namespaces/$ns/deployments/dev-e2e-target?fieldManager=unidesk-ci&force=true" >/dev/null
|
||||
kube PATCH \
|
||||
-H "Content-Type: application/apply-patch+yaml" \
|
||||
--data-binary @/tmp/dev-e2e-target-01 \
|
||||
"$kube_api/api/v1/namespaces/$ns/services/dev-e2e-target?fieldManager=unidesk-ci&force=true" >/dev/null
|
||||
|
||||
deadline=$((SECONDS + 180))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
status="$(kube GET "$kube_api/apis/apps/v1/namespaces/$ns/deployments/dev-e2e-target")"
|
||||
available="$(printf '%s' "$status" | jq -r '.status.availableReplicas // 0')"
|
||||
observed="$(printf '%s' "$status" | jq -r '.status.observedGeneration // 0')"
|
||||
generation="$(printf '%s' "$status" | jq -r '.metadata.generation // 0')"
|
||||
if [ "$available" -ge 1 ] && [ "$observed" -ge "$generation" ]; then
|
||||
echo "dev_e2e_target_rollout=available namespace=$ns"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$SECONDS" -ge "$deadline" ]; then
|
||||
echo "dev_e2e_target_rollout=timeout namespace=$ns" >&2
|
||||
kube GET "$kube_api/apis/apps/v1/namespaces/$ns/deployments/dev-e2e-target" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bun - "$ns" "$(params.deploy-commit)" "$backend_commit" "$frontend_commit" "$code_queue_commit" "$result_json" <<'BUN'
|
||||
const [ns, deployCommit, backendCommit, frontendCommit, codeQueueCommit, resultPath] = process.argv.slice(2);
|
||||
const url = `http://dev-e2e-target.${ns}.svc.cluster.local:8080/health`;
|
||||
const started = performance.now();
|
||||
const response = await fetch(url);
|
||||
const elapsedMs = Math.round(performance.now() - started);
|
||||
const body = await response.json();
|
||||
const checks = [
|
||||
response.ok,
|
||||
body.ok === true,
|
||||
body.environment === "dev",
|
||||
body.namespace === ns,
|
||||
body.deployCommit === deployCommit,
|
||||
body.backendCoreCommit === backendCommit,
|
||||
body.frontendCommit === frontendCommit,
|
||||
body.codeQueueCommit === codeQueueCommit
|
||||
];
|
||||
const result = { ok: checks.every(Boolean), elapsedMs, url, body };
|
||||
await Bun.write(resultPath, JSON.stringify(result, null, 2) + "\n");
|
||||
console.log(JSON.stringify(result));
|
||||
if (!result.ok) process.exit(1);
|
||||
BUN
|
||||
---
|
||||
apiVersion: tekton.dev/v1
|
||||
kind: Pipeline
|
||||
metadata:
|
||||
name: unidesk-dev-namespace-e2e
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-ci
|
||||
app.kubernetes.io/component: dev-namespace-e2e
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
spec:
|
||||
params:
|
||||
- name: repo-url
|
||||
type: string
|
||||
default: https://github.com/pikasTech/unidesk
|
||||
- name: desired-ref
|
||||
type: string
|
||||
default: master
|
||||
- name: deploy-commit
|
||||
type: string
|
||||
- name: environment
|
||||
type: string
|
||||
default: dev
|
||||
- name: run-id
|
||||
type: string
|
||||
- name: keep-namespace
|
||||
type: string
|
||||
default: "false"
|
||||
- name: app-image
|
||||
type: string
|
||||
default: unidesk-code-queue:dev
|
||||
workspaces:
|
||||
- name: shared-workspace
|
||||
tasks:
|
||||
- name: dev-namespace-e2e
|
||||
taskRef:
|
||||
name: unidesk-dev-namespace-e2e
|
||||
params:
|
||||
- name: repo-url
|
||||
value: "$(params.repo-url)"
|
||||
- name: desired-ref
|
||||
value: "$(params.desired-ref)"
|
||||
- name: deploy-commit
|
||||
value: "$(params.deploy-commit)"
|
||||
- name: environment
|
||||
value: "$(params.environment)"
|
||||
- name: run-id
|
||||
value: "$(params.run-id)"
|
||||
- name: keep-namespace
|
||||
value: "$(params.keep-namespace)"
|
||||
- name: app-image
|
||||
value: "$(params.app-image)"
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: shared-workspace
|
||||
---
|
||||
apiVersion: tekton.dev/v1
|
||||
kind: Pipeline
|
||||
metadata:
|
||||
name: unidesk-ci
|
||||
@@ -604,10 +1044,10 @@ spec:
|
||||
type: string
|
||||
- name: check-image
|
||||
type: string
|
||||
default: unidesk-code-queue:d601
|
||||
default: unidesk-code-queue:dev
|
||||
- name: code-queue-image
|
||||
type: string
|
||||
default: unidesk-code-queue:d601
|
||||
default: unidesk-code-queue:dev
|
||||
workspaces:
|
||||
- name: shared-workspace
|
||||
tasks:
|
||||
|
||||
+22
-22
@@ -155,8 +155,8 @@ metadata:
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/deploy-service-id: code-queue
|
||||
annotations:
|
||||
unidesk.ai/deploy-ref: origin/deploy/dev
|
||||
unidesk.ai/image-source: deploy-dev-commit
|
||||
unidesk.ai/deploy-ref: origin/master:deploy.json#environments.dev
|
||||
unidesk.ai/image-source: deploy-env-commit
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
@@ -205,9 +205,9 @@ spec:
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: https://github.com/pikasTech/unidesk
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /etc/unidesk-provider-egress
|
||||
@@ -250,8 +250,8 @@ metadata:
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/deploy-service-id: code-queue
|
||||
annotations:
|
||||
unidesk.ai/deploy-ref: origin/deploy/dev
|
||||
unidesk.ai/image-source: deploy-dev-commit
|
||||
unidesk.ai/deploy-ref: origin/master:deploy.json#environments.dev
|
||||
unidesk.ai/image-source: deploy-env-commit
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
@@ -306,13 +306,13 @@ spec:
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: https://github.com/pikasTech/unidesk
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
value: D601-dev
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
@@ -460,8 +460,8 @@ metadata:
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/deploy-service-id: code-queue
|
||||
annotations:
|
||||
unidesk.ai/deploy-ref: origin/deploy/dev
|
||||
unidesk.ai/image-source: deploy-dev-commit
|
||||
unidesk.ai/deploy-ref: origin/master:deploy.json#environments.dev
|
||||
unidesk.ai/image-source: deploy-env-commit
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -514,13 +514,13 @@ spec:
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: https://github.com/pikasTech/unidesk
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
value: D601-dev-read
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
@@ -603,8 +603,8 @@ metadata:
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/deploy-service-id: code-queue
|
||||
annotations:
|
||||
unidesk.ai/deploy-ref: origin/deploy/dev
|
||||
unidesk.ai/image-source: deploy-dev-commit
|
||||
unidesk.ai/deploy-ref: origin/master:deploy.json#environments.dev
|
||||
unidesk.ai/image-source: deploy-env-commit
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -657,13 +657,13 @@ spec:
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: https://github.com/pikasTech/unidesk
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
value: D601-dev-write
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
|
||||
@@ -32,8 +32,8 @@ metadata:
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/environment: dev
|
||||
annotations:
|
||||
unidesk.ai/deploy-ref: origin/deploy/dev
|
||||
unidesk.ai/image-source: deploy-dev-commit
|
||||
unidesk.ai/deploy-ref: origin/master:deploy.json#environments.dev
|
||||
unidesk.ai/image-source: deploy-env-commit
|
||||
unidesk.ai/deploy-service-id: backend-core
|
||||
spec:
|
||||
replicas: 1
|
||||
@@ -105,9 +105,9 @@ spec:
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: https://github.com/pikasTech/unidesk
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: LOG_FILE
|
||||
value: /var/log/unidesk-dev/backend-core-dev.jsonl
|
||||
volumeMounts:
|
||||
@@ -175,8 +175,8 @@ metadata:
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/environment: dev
|
||||
annotations:
|
||||
unidesk.ai/deploy-ref: origin/deploy/dev
|
||||
unidesk.ai/image-source: deploy-dev-commit
|
||||
unidesk.ai/deploy-ref: origin/master:deploy.json#environments.dev
|
||||
unidesk.ai/image-source: deploy-env-commit
|
||||
unidesk.ai/deploy-service-id: frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
@@ -250,9 +250,9 @@ spec:
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: https://github.com/pikasTech/unidesk
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-dev-commit
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: LOG_FILE
|
||||
value: /var/log/unidesk-dev/frontend-dev.jsonl
|
||||
volumeMounts:
|
||||
|
||||
@@ -82,7 +82,7 @@ metadata:
|
||||
data:
|
||||
UNIDESK_ENV: dev
|
||||
UNIDESK_NAMESPACE: unidesk-dev
|
||||
UNIDESK_DEPLOY_REF: origin/deploy/dev
|
||||
UNIDESK_DEPLOY_REF: origin/master:deploy.json#environments.dev
|
||||
UNIDESK_PROVIDER_ID: D601-dev
|
||||
UNIDESK_NODE_ID: D601
|
||||
UNIDESK_DEV_DATABASE_NAME: unidesk_dev
|
||||
@@ -177,7 +177,7 @@ data:
|
||||
('namespace', 'unidesk-dev', now()),
|
||||
('database', 'unidesk_dev', now()),
|
||||
('provider_id', 'D601-dev', now()),
|
||||
('deploy_ref', 'origin/deploy/dev', now())
|
||||
('deploy_ref', 'origin/master:deploy.json#environments.dev', now())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS unidesk_nodes (
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "devops",
|
||||
"namespace": "unidesk-ci"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
"serviceName": "devops",
|
||||
"servicePort": 4286
|
||||
},
|
||||
"activeInstanceId": "D601",
|
||||
"singleWriter": true,
|
||||
"expectedNodeIds": [
|
||||
"D601"
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "D601",
|
||||
"nodeId": "D601",
|
||||
"role": "primary",
|
||||
"baseUrl": "kubernetes://unidesk-ci/services/devops:4286",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "service-proxy"
|
||||
}
|
||||
],
|
||||
"requireAllInstancesHealthy": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: devops
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: devops
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: devops
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: devops
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: ["tekton.dev"]
|
||||
resources: ["pipelines", "tasks", "taskruns"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["tekton.dev"]
|
||||
resources: ["pipelineruns"]
|
||||
verbs: ["get", "list", "watch", "create"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: devops
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: devops
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: devops
|
||||
namespace: unidesk-ci
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: devops
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: devops
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: devops
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601
|
||||
unidesk.ai/deploy-service-id: devops
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: devops
|
||||
unidesk.ai/instance-id: D601
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: devops
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601
|
||||
unidesk.ai/node-id: D601
|
||||
unidesk.ai/deploy-service-id: devops
|
||||
annotations:
|
||||
unidesk.ai/deploy-service-id: devops
|
||||
unidesk.ai/deploy-repo: https://github.com/pikasTech/unidesk
|
||||
unidesk.ai/deploy-commit: replace-with-deploy-env-commit
|
||||
unidesk.ai/deploy-requested-commit: replace-with-deploy-env-commit
|
||||
spec:
|
||||
serviceAccountName: devops
|
||||
nodeSelector:
|
||||
unidesk.ai/node-id: D601
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: devops
|
||||
image: unidesk-devops:dev-placeholder
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 4286
|
||||
env:
|
||||
- name: HOST
|
||||
value: "0.0.0.0"
|
||||
- name: PORT
|
||||
value: "4286"
|
||||
- name: DEVOPS_NAMESPACE
|
||||
value: "unidesk-ci"
|
||||
- name: DEVOPS_REPO_URL
|
||||
value: "https://github.com/pikasTech/unidesk"
|
||||
- name: DEVOPS_DESIRED_REF
|
||||
value: "master"
|
||||
- name: DEVOPS_ENVIRONMENT
|
||||
value: "dev"
|
||||
- name: UNIDESK_DEPLOY_SERVICE_ID
|
||||
value: "devops"
|
||||
- name: UNIDESK_DEPLOY_REPO
|
||||
value: "https://github.com/pikasTech/unidesk"
|
||||
- name: UNIDESK_DEPLOY_COMMIT
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
||||
value: replace-with-deploy-env-commit
|
||||
- name: DEVOPS_GIT_PROXY_URL
|
||||
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
|
||||
- name: DEVOPS_DEV_E2E_PIPELINE
|
||||
value: "unidesk-dev-namespace-e2e"
|
||||
- name: DEVOPS_PIPELINE_SERVICE_ACCOUNT
|
||||
value: "unidesk-ci-runner"
|
||||
- name: DEVOPS_PIPELINE_WORKSPACE_CLAIM
|
||||
value: "unidesk-ci-cache"
|
||||
- name: DEVOPS_DEV_E2E_APP_IMAGE
|
||||
value: "unidesk-code-queue:dev"
|
||||
- name: LOG_FILE
|
||||
value: "/var/log/unidesk/devops.jsonl"
|
||||
volumeMounts:
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 12
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 6
|
||||
resources:
|
||||
requests:
|
||||
cpu: 20m
|
||||
memory: 48Mi
|
||||
limits:
|
||||
memory: 160Mi
|
||||
volumes:
|
||||
- name: logs
|
||||
hostPath:
|
||||
path: /home/ubuntu/.unidesk/devops-deploy/logs
|
||||
type: DirectoryOrCreate
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: devops
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: devops
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: devops
|
||||
unidesk.ai/instance-id: D601
|
||||
ports:
|
||||
- name: http
|
||||
port: 4286
|
||||
targetPort: http
|
||||
@@ -274,7 +274,7 @@ function mergeServices(services: ManagedService[]): ManagedService[] {
|
||||
}
|
||||
|
||||
function readConfig(): RuntimeConfig {
|
||||
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json"));
|
||||
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json,k3s/devops.k3s.json"));
|
||||
const inlineServices = parseServices(envString("K3SCTL_SERVICES_JSON", "[]"));
|
||||
const manifestServices = readManifestServices(paths);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user