diff --git a/.github/workflows/buildAndPublish.yml b/.github/workflows/build-and-publish.yml similarity index 90% rename from .github/workflows/buildAndPublish.yml rename to .github/workflows/build-and-publish.yml index 75b38a5..eff3bd5 100644 --- a/.github/workflows/buildAndPublish.yml +++ b/.github/workflows/build-and-publish.yml @@ -1,17 +1,19 @@ +name: Build and publish + on: create: tags: - '*' push: branches: - - master + - main env: IMAGE_NAME: rancher-redeploy-workload jobs: build: - name: Build and deploy + name: Build and publish runs-on: ubuntu-latest steps: @@ -38,7 +40,7 @@ jobs: [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') # Use Docker `latest` tag convention - [ "$VERSION" == "master" ] && VERSION=latest + [ "$VERSION" == "main" ] && VERSION=latest echo IMAGE_ID=$IMAGE_ID echo VERSION=$VERSION diff --git a/Dockerfile b/Dockerfile index e3df144..8c65af2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,25 @@ -FROM python:3.9 +FROM golang:1.16 -WORKDIR /rancher-redeploy-workload +WORKDIR /usr/src/rancher-redeploy-workload -COPY requirements.txt requirements.txt +COPY go.mod go.sum ./ +RUN go mod download -RUN pip install -r requirements.txt +COPY config.go . +COPY main.go . +COPY validator.go . -COPY redeploy_rancher_workload.py redeploy_rancher_workload.py +RUN mkdir bin + +RUN CGO_ENABLED=0 go build -a -o bin/rancher-redeploy-workload . + +FROM alpine:latest + +RUN apk update && apk add bash + +COPY --from=0 /usr/src/rancher-redeploy-workload/bin/rancher-redeploy-workload /usr/local/bin/rancher-redeploy-workload -# docker-entrypoint.sh COPY docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh -ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file +CMD ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md diff --git a/config.go b/config.go new file mode 100644 index 0000000..312f051 --- /dev/null +++ b/config.go @@ -0,0 +1,69 @@ +package main + +import ( + "errors" + "fmt" + "os" + "reflect" +) + +type Config struct { + Debug string `env:"DEBUG"` + RancherBearerToken string `env:"RANCHER_BEARER_TOKEN" validate:"required"` + RancherClusterId string `env:"RANCHER_CLUSTER_ID" validate:"required"` + RancherNamespace string `env:"RANCHER_NAMESPACE" validate:"required"` + RancherProjectId string `env:"RANCHER_PROJECT_ID" validate:"required"` + RancherUrl string `env:"RANCHER_URL" validate:"required"` + RancherWorkloads string `env:"RANCHER_WORKLOADS" validate:"required"` +} + +func NewConfig(v *Validator) (*Config, error) { + c := Config{} + + v, err := NewValidator() + if err != nil { + return nil, err + } + + t := reflect.TypeOf(c) + vp := reflect.ValueOf(&c) + + for i := 0; i < t.NumField(); i++ { + ti := t.Field(i) + vpi := vp.Elem().Field(i) + + envVarKey := ti.Tag.Get("env") + + if envVarKey != "" { + envVarValue := os.Getenv(envVarKey) + + vpi.SetString(envVarValue) + } + } + + err = v.Validate.Struct(c) + if err != nil { + output := "There are errors with some environment variables:\n" + + for fieldName, fieldMessage := range v.Map(err) { + structField, _ := t.FieldByName(fieldName) + + output = output + fmt.Sprintf("* %s: %s\n", structField.Tag.Get("env"), fieldMessage) + } + + return nil, errors.New(output) + } + + return &c, nil +} + +func (c *Config) generateWorkloadRedeployUrl(workload string) string { + return fmt.Sprintf( + "%s/v3/project/%s:%s/workloads/deployment:%s:%s?action=redeploy", + c.RancherUrl, + c.RancherClusterId, + c.RancherProjectId, + c.RancherNamespace, + workload, + ) +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9a4f1fa..d1f08fe 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -eo pipefail -exec python /rancher-redeploy-workload/redeploy_rancher_workload.py +exec /usr/local/bin/rancher-redeploy-workload diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e8f0603 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/th0th/rancher-redeploy-workload + +go 1.16 + +require ( + github.com/go-playground/locales v0.13.0 + github.com/go-playground/universal-translator v0.17.0 + github.com/go-playground/validator/v10 v10.6.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fc37b75 --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I= +github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1ae3abe --- /dev/null +++ b/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "io/ioutil" + "log" + "net/http" + "strings" +) + +func main() { + v, err := NewValidator() + if err != nil { + log.Panic(err) + } + + c, err := NewConfig(v) + if err != nil { + log.Panicln(err) + } + + workloads := strings.Split(c.RancherWorkloads, ",") + + log.Println("Workloads to redeploy:") + + for _, workload := range workloads { + log.Println("* " + workload) + } + + log.Println("Staring to redeploy...") + + hasErrors := false + + for _, workload := range workloads { + req, err := http.NewRequest(http.MethodPost, c.generateWorkloadRedeployUrl(workload), strings.NewReader("{}")) + if err != nil { + log.Panic(err) + } + + req.Header.Set("Authorization", "Bearer "+c.RancherBearerToken) + + rsp, err := http.DefaultClient.Do(req) + if rsp != nil && rsp.StatusCode == 200 { + log.Println("✅ " + workload) + } else { + hasErrors = true + + log.Println("❌ " + workload) + + if c.Debug == "true" { + if err != nil { + log.Print(err) + } else { + body, err := ioutil.ReadAll(rsp.Body) + if err != nil { + log.Panic(err) + } + + log.Println("Status code: " + rsp.Status) + log.Println("Body: " + string(body)) + } + } + + } + } + + if hasErrors { + if c.Debug != "true" { + log.Println("In order to log the errors, please set DEBUG environment variable as 'true',") + } + } +} diff --git a/redeploy_rancher_workload.py b/redeploy_rancher_workload.py deleted file mode 100644 index d44cd90..0000000 --- a/redeploy_rancher_workload.py +++ /dev/null @@ -1,87 +0,0 @@ -from datetime import datetime -import logging -import os -import sys -from typing import List - -import requests - -logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s', level=logging.INFO) - -required_environment_variables: List[str] = [ - 'RANCHER_BEARER_TOKEN', - 'RANCHER_CLUSTER_ID', - 'RANCHER_NAMESPACE', - 'RANCHER_PROJECT_ID', - 'RANCHER_URL', - 'RANCHER_WORKLOADS', -] - -missing_environment_variables: List[str] = [] - -for required_environment_variable in required_environment_variables: - if required_environment_variable not in os.environ: - missing_environment_variables.append(required_environment_variable) - -if len(missing_environment_variables) > 0: - logging.error("These environment variables are required but not set: {missing_environment_variables}".format( - missing_environment_variables=', '.join(missing_environment_variables), - )) - - sys.exit(1) - -rancher_bearer_token = os.environ['RANCHER_BEARER_TOKEN'] -rancher_cluster_id = os.environ['RANCHER_CLUSTER_ID'] -rancher_namespace = os.environ['RANCHER_NAMESPACE'] -rancher_project_id = os.environ['RANCHER_PROJECT_ID'] -rancher_url = os.environ['RANCHER_URL'] -rancher_workloads = os.environ['RANCHER_WORKLOADS'] - - -def generate_workload_url(r_workload: str) -> str: - return ( - '{rancher_url}/v3/project/{rancher_cluster_id}:{rancher_project_id}' - '/workloads/deployment:{rancher_namespace}:{rancher_workload}' - ).format( - rancher_cluster_id=rancher_cluster_id, - rancher_namespace=rancher_namespace, - rancher_project_id=rancher_project_id, - rancher_url=rancher_url, - rancher_workload=r_workload, - ) - - -headers = { - 'Authorization': 'Bearer {rancher_bearer_token}'.format( - rancher_bearer_token=rancher_bearer_token, - ), -} - -for rancher_workload in rancher_workloads.split(','): - url = generate_workload_url(rancher_workload) - - response_get = requests.get( - headers={ - **headers - }, - url=url, - ) - - response_get.raise_for_status() - workload = response_get.json() - - workload['annotations']['cattle.io/timestamp'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') - - response_put = requests.put( - headers={ - **headers, - }, - json=workload, - url=url, - ) - - response_put.raise_for_status() - - logging.info("Workload {rancher_workload} is successfully redeployed.".format( - rancher_workload=rancher_workload, - )) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7d34355..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.25.0 diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..c9ad047 --- /dev/null +++ b/validator.go @@ -0,0 +1,70 @@ +package main + +import ( + "github.com/go-playground/locales/en" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + "log" +) + +type Validator struct { + Translator ut.Translator + Validate *validator.Validate +} + +func NewValidator() (*Validator, error) { + validate := validator.New() + + enLocale := en.New() + u := ut.New(enLocale, enLocale) + + trans, _ := u.GetTranslator("en") + + err := enTranslations.RegisterDefaultTranslations(validate, trans) + if err != nil { + return nil, err + } + + v := &Validator{ + Translator: trans, + Validate: validate, + } + + err = v.RegisterTranslations() + if err != nil { + return nil, err + } + + return v, nil +} + +func (v *Validator) Map(err error) map[string]interface{} { + m := map[string]interface{}{} + + validationErrors := err.(validator.ValidationErrors) + + for _, e := range validationErrors { + m[e.Field()] = e.Translate(v.Translator) + } + + return m +} + +func (v *Validator) RegisterTranslations() error { + // required + return v.Validate.RegisterTranslation( + "required", + v.Translator, + func(u ut.Translator) error { + return u.Add("required", "This field is required.", true) + }, + func(u ut.Translator, fe validator.FieldError) string { + t, err := u.T("required", fe.Field()) + if err != nil { + log.Panic(err) + } + + return t + }) +}