Created
March 5, 2026 22:26
-
-
Save mickael-kerjean/eac61853bc1f67fdb455cc22982fde62 to your computer and use it in GitHub Desktop.
k8s
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package model | |
| import ( | |
| "bytes" | |
| "crypto/tls" | |
| "crypto/x509" | |
| "encoding/base64" | |
| "encoding/json" | |
| "fmt" | |
| "github.com/mickael-kerjean/filestash-deploy/src/utils" | |
| "golang.org/x/crypto/bcrypt" | |
| "io/ioutil" | |
| "net/http" | |
| "os" | |
| "strings" | |
| "time" | |
| ) | |
| func CreateK8sConfigAndDeploy(app utils.AppConfig) (err error) { | |
| timer := time.Now() | |
| domain := app.Domain | |
| fmt.Printf("[DEPLOYMENT] START - %s\n", domain) | |
| defer func() { | |
| fmt.Printf( | |
| "[DEPLOYMENT] COMPLETE - %s complete in %d sec - error: %s\n", | |
| domain, | |
| int(time.Since(timer).Seconds()), | |
| func() string { | |
| if err != nil { | |
| err.Error() | |
| } | |
| return "N/A" | |
| }(), | |
| ) | |
| }() | |
| hashedPassword, err := bcrypt.GenerateFromPassword([]byte(app.Password), bcrypt.DefaultCost) | |
| if err != nil { | |
| return err | |
| } | |
| app.Password = string(hashedPassword) | |
| if err = createK8sDeployment(app); err != nil { | |
| deleteK8sDeployment(domain) | |
| return err | |
| } | |
| if err = createK8sService(domain); err != nil { | |
| deleteK8sService(domain) | |
| deleteK8sDeployment(domain) | |
| return err | |
| } | |
| if err = createK8sIngress(domain); err != nil { | |
| deleteK8sService(domain) | |
| deleteK8sDeployment(domain) | |
| deleteK8sIngress(domain) | |
| return err | |
| } | |
| if err = waitK8sDeployment(domain); err != nil { | |
| deleteK8sIngress(domain) | |
| deleteK8sService(domain) | |
| deleteK8sDeployment(domain) | |
| return err | |
| } | |
| return executeHealthCheck(domain) | |
| } | |
| func RemoveK8sDeployment(domain string) (err error) { | |
| if tmpErr := deleteK8sIngress(domain); err != nil { | |
| err = tmpErr | |
| } | |
| if tmpErr := deleteK8sService(domain); err != nil { | |
| err = tmpErr | |
| } | |
| if tmpErr := deleteK8sDeployment(domain); err != nil { | |
| err = tmpErr | |
| } | |
| return err | |
| } | |
| func RestartK8sDeployment(domain string) error { | |
| return k8sExecute( | |
| "PATCH", | |
| "/apis/apps/v1/namespaces/filestash-cloud/deployments/"+domain, | |
| fmt.Sprintf( | |
| `{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"%s"}}}}}`, | |
| time.Now().Format(time.RFC3339), | |
| ), | |
| ) | |
| } | |
| func createK8sDeployment(app utils.AppConfig) error { | |
| connectionDefault := func(a utils.AppConfig) string { | |
| switch a.BackendType { | |
| case "sftp": | |
| if a.BackendHost != "" { | |
| return fmt.Sprintf(`[{"type": "sftp", "label": "SFTP", "hostname": "%s", "advanced": true, "path": null, "port": null, "passphrase": null, "hostkey": null}]`, app.BackendHost) | |
| } | |
| return `[{"type": "sftp", "label": "SFTP", "advanced": true, "path": null, "port": null, "passphrase": null, "hostkey": null}]` | |
| case "ftp": | |
| if a.BackendHost != "" { | |
| return fmt.Sprintf(`[{"type": "ftp", "label": "FTP", "hostname": "%s", "advanced": true, "path": null, "port": null, "conn": null}]`, app.BackendHost) | |
| } | |
| return `[{"type": "ftp", "label": "FTP", "advanced": true, "path": null, "port": null, "conn": null}]` | |
| case "s3": | |
| return `[{"type": "s3", "label": "S3", "advanced": true, "path": null, "encryption_key": null, "region": null, "endpoint": null}]` | |
| case "git": | |
| return `[{"type": "git", "label": "GIT", "advanced": true, "path": null, "passphrase": null, "commit": null, "branch": null, "author_email": null, "author_name": null, "committer_email": null, "committer_name": null}]` | |
| case "minio": | |
| if a.BackendHost != "" { | |
| return fmt.Sprintf(`[{"type": "s3", "label": "MINIO", "advanced": true, "path": null, "encryption_key": null, "region": null, "endpoint": "%s"}]`, app.BackendHost) | |
| } | |
| return `[{"type": "s3", "label": "MINIO", "advanced": true, "path": null, "encryption_key": null, "region": null}]` | |
| case "webdav": | |
| if a.BackendHost != "" { | |
| return fmt.Sprintf(`[{"type": "webdav", "label": "Webdav", "advanced": true, "path": null, "url": "%s"}]`, app.BackendHost) | |
| } | |
| return `[{"type": "webdav", "label": "Webdav", "advanced": true, "path": null}]` | |
| default: | |
| return `[{"type": "s3", "label": "S3"}, {"type": "sftp", "label": "SFTP"}, {"type": "ftp", "label": "FTP"}, {"type": "git", "label": "GIT"}]` | |
| } | |
| } | |
| escape := func(j string) string { | |
| return strings.ReplaceAll(j, `"`, `\"`) | |
| } | |
| return k8sExecute( | |
| "POST", | |
| "/apis/apps/v1/namespaces/filestash-cloud/deployments/", | |
| strings.ReplaceAll(`{ | |
| "apiVersion": "apps/v1", | |
| "kind": "Deployment", | |
| "metadata": { | |
| "labels": { | |
| "app": "{{ .Name }}" | |
| }, | |
| "name": "{{ .Name }}", | |
| "namespace": "filestash-cloud" | |
| }, | |
| "spec": { | |
| "replicas": 1, | |
| "selector": { | |
| "matchLabels": { | |
| "app": "{{ .Name }}" | |
| } | |
| }, | |
| "template": { | |
| "metadata": { | |
| "labels": { | |
| "app": "{{ .Name }}" | |
| } | |
| }, | |
| "spec": { | |
| "containers": [ | |
| { | |
| "image": "machines/filestash:latest", | |
| "name": "{{ .Name }}", | |
| "env": [ | |
| { | |
| "name": "ADMIN_PASSWORD", | |
| "value": "`+app.Password+`" | |
| }, | |
| { | |
| "name": "APPLICATION_URL", | |
| "value": "{{ .Name }}.filestash.app" | |
| }, | |
| { | |
| "name": "ONLYOFFICE_URL", | |
| "value": "http://me-kerjean-files-onlyoffice.selfhosted.svc.cluster.local" | |
| } | |
| ], | |
| "livenessProbe": { | |
| "failureThreshold": 3, | |
| "httpGet": { | |
| "path": "/healthz", | |
| "port": 8334, | |
| "scheme": "HTTP" | |
| }, | |
| "initialDelaySeconds": 60, | |
| "periodSeconds": 30 | |
| }, | |
| "volumeMounts": [{ | |
| "mountPath": "/app/data/state", | |
| "name": "filestash-cloud-{{ .Name }}-volume" | |
| }] | |
| } | |
| ], | |
| "initContainers": [{ | |
| "name": "filestash-cloud-init-{{ .Name }}", | |
| "image": "alpine", | |
| "command": ["sh", "-c"], | |
| "args": ["mkdir -p /mnt/config/ && if [ ! -s /mnt/config/config.json ]; then echo '{\"general\": {\"fork_button\": false}, \"connections\":`+escape(connectionDefault(app))+`}' > /mnt/config/config.json; fi"], | |
| "volumeMounts": [{ | |
| "mountPath": "/mnt/", | |
| "name": "filestash-cloud-{{ .Name }}-volume" | |
| }] | |
| }], | |
| "volumes": [ | |
| { | |
| "hostPath": { | |
| "path": "/mnt/app-filestash-cloud/{{ .Name }}", | |
| "type": "DirectoryOrCreate" | |
| }, | |
| "name": "filestash-cloud-{{ .Name }}-volume" | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| }`, "{{ .Name }}", app.Domain)) | |
| } | |
| func deleteK8sDeployment(domain string) error { | |
| return k8sExecute("DELETE", "/apis/apps/v1/namespaces/filestash-cloud/deployments/"+domain, "") | |
| } | |
| func waitK8sDeployment(domain string) (err error) { | |
| for i := 0; i < 200; i++ { | |
| time.Sleep(500 * time.Millisecond) | |
| content, err := k8sQuery("GET", "/apis/apps/v1/namespaces/filestash-cloud/deployments/"+domain, "") | |
| data := struct { | |
| Status struct { | |
| ReadyReplicas int `json:"readyReplicas"` | |
| Replicas int `json:"replicas"` | |
| } `json:"status"` | |
| }{} | |
| if err = json.Unmarshal(content, &data); err != nil { | |
| return err | |
| } | |
| if data.Status.Replicas == data.Status.ReadyReplicas { | |
| if data.Status.Replicas != 0 { | |
| return nil | |
| } | |
| return fmt.Errorf("No replicas at all?!?") | |
| } | |
| } | |
| return fmt.Errorf("deployment not set") | |
| } | |
| func createK8sService(domain string) error { | |
| return k8sExecute( | |
| "POST", | |
| "/api/v1/namespaces/filestash-cloud/services/", strings.ReplaceAll(` | |
| { | |
| "apiVersion": "v1", | |
| "kind": "Service", | |
| "metadata": { | |
| "labels": { | |
| "app": "{{ .Name }}" | |
| }, | |
| "name": "{{ .Name }}", | |
| "namespace": "filestash-cloud" | |
| }, | |
| "spec": { | |
| "type": "ClusterIP", | |
| "ports": [{ | |
| "port": 8334, | |
| "protocol": "TCP", | |
| "targetPort": 8334 | |
| }], | |
| "selector": { | |
| "app": "{{ .Name }}" | |
| } | |
| } | |
| }`, "{{ .Name }}", domain)) | |
| } | |
| func deleteK8sService(domain string) error { | |
| return k8sExecute("DELETE", "/api/v1/namespaces/filestash-cloud/services/"+domain, "") | |
| } | |
| func createK8sIngress(domain string) error { | |
| return k8sExecute( | |
| "POST", | |
| "/apis/extensions/v1beta1/namespaces/filestash-cloud/ingresses/", strings.ReplaceAll(` | |
| { | |
| "apiVersion": "extensions/v1beta1", | |
| "kind": "Ingress", | |
| "metadata": { | |
| "annotations": { | |
| "ingress.kubernetes.io/ssl-redirect": "true", | |
| "kubernetes.io/ingress.class": "nginx" | |
| }, | |
| "name": "{{ .Name }}", | |
| "namespace": "filestash-cloud" | |
| }, | |
| "spec": { | |
| "rules": [{ | |
| "host": "{{ .Name }}.filestash.app", | |
| "http": { | |
| "paths": [{ | |
| "backend": { | |
| "serviceName": "{{ .Name }}", | |
| "servicePort": 8334 | |
| } | |
| }] | |
| } | |
| }], | |
| "tls": [{ | |
| "hosts": [ | |
| "{{ .Name }}.filestash.app" | |
| ], | |
| "secretName": "app-filestash-wildcard" | |
| }] | |
| } | |
| }`, "{{ .Name }}", domain)) | |
| } | |
| func deleteK8sIngress(domain string) error { | |
| return k8sExecute("DELETE", "/apis/extensions/v1beta1/namespaces/filestash-cloud/ingresses/"+domain, "") | |
| } | |
| func executeHealthCheck(domain string) error { | |
| for i := 0; i < 100; i++ { | |
| time.Sleep(400 * time.Millisecond) | |
| client := &http.Client{} | |
| req, err := http.NewRequest("GET", fmt.Sprintf("https://%s.filestash.app", domain), nil) | |
| if err != nil { | |
| fmt.Printf("REQUEST CREATION ERR '%s'\n", err.Error()) | |
| return err | |
| } | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| fmt.Printf("REQUEST SENT ERR '%s'\n", err.Error()) | |
| continue | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode == 200 { | |
| return nil | |
| } | |
| } | |
| return fmt.Errorf("Health check hasn't complete") | |
| } | |
| /* INTERNAL */ | |
| func k8sExecute(method string, uri string, j string) error { | |
| _, err := k8sQuery(method, uri, j) | |
| return err | |
| } | |
| func k8sQuery(method string, uri string, j string) ([]byte, error) { | |
| apiServer := os.Getenv("API_SERVER") | |
| if apiServer == "" { | |
| return []byte(""), fmt.Errorf("[ENV] missing 'API_SERVER'") | |
| } | |
| caCert64 := os.Getenv("CA_CERT") | |
| if caCert64 == "" { | |
| return []byte(""), fmt.Errorf("[env] missing 'CA_CERT'") | |
| } | |
| token := os.Getenv("TOKEN") | |
| if caCert64 == "" { | |
| return []byte(""), fmt.Errorf("[env] missing 'TOKEN'") | |
| } | |
| caCert, err := base64.StdEncoding.DecodeString(caCert64) | |
| caCertPool := x509.NewCertPool() | |
| caCertPool.AppendCertsFromPEM(caCert) | |
| req, err := http.NewRequest(method, apiServer+uri, bytes.NewBuffer([]byte(j))) | |
| req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) | |
| if err != nil { | |
| return []byte(""), err | |
| } | |
| if method == "PATCH" { | |
| req.Header.Set("Content-Type", "application/strategic-merge-patch+json") | |
| } | |
| resp, err := (&http.Client{ | |
| Transport: &http.Transport{ | |
| TLSClientConfig: &tls.Config{ | |
| RootCAs: caCertPool, | |
| }, | |
| }, | |
| }).Do(req) | |
| if err != nil { | |
| return []byte(""), fmt.Errorf("Request error - %s", err.Error()) | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != 201 && resp.StatusCode != 200 { | |
| b, _ := ioutil.ReadAll(resp.Body) | |
| return []byte(""), fmt.Errorf("HTTP code is '%d'\n\n%s", resp.StatusCode, b) | |
| } | |
| return ioutil.ReadAll(resp.Body) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment