Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mickael-kerjean/eac61853bc1f67fdb455cc22982fde62 to your computer and use it in GitHub Desktop.

Select an option

Save mickael-kerjean/eac61853bc1f67fdb455cc22982fde62 to your computer and use it in GitHub Desktop.
k8s
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