diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 94ada66..c402f81 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,11 +15,11 @@ jobs: - ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version-file: 'go.mod' - name: lint @@ -36,8 +36,11 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 - - name: docker build - run: docker build . + - name: Build container image + uses: docker/build-push-action@v5 + with: + push: false + tags: ${{ steps.meta.outputs.tags }} \ No newline at end of file diff --git a/cmd/server/cmd.go b/cmd/server/cmd.go index 0c9a3f4..b243968 100644 --- a/cmd/server/cmd.go +++ b/cmd/server/cmd.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/datastore/mysql" "github.com/whywaita/myshoes/pkg/gh" @@ -22,6 +22,9 @@ import ( func init() { config.Load() + mysqlURL := config.LoadMySQLURL() + config.Config.MySQLDSN = mysqlURL + if err := gh.InitializeCache(config.Config.GitHub.AppID, config.Config.GitHub.PEMByte); err != nil { log.Panicf("failed to create a cache: %+v", err) } diff --git a/docs/01_01_for_admin_setup.md b/docs/01_01_for_admin_setup.md index 3c56c33..b49058f 100644 --- a/docs/01_01_for_admin_setup.md +++ b/docs/01_01_for_admin_setup.md @@ -89,6 +89,9 @@ A config variables can set from environment values. - required - set path of myshoes-provider binary. - example) `./shoes-mock` `https://example.com/shoes-mock` `https://github.com/whywaita/myshoes-providers/releases/download/v0.1.0/shoes-lxd-linux-amd64` +- `PLUGIN_OUTPUT` + - default: `.` + - set path of directory that contains myshoes-provider binary. - `GITHUB_URL` - default: `https://github.com` - The URL of GitHub Enterprise Server. diff --git a/internal/config/config.go b/pkg/config/config.go similarity index 86% rename from internal/config/config.go rename to pkg/config/config.go index baef944..8149095 100644 --- a/internal/config/config.go +++ b/pkg/config/config.go @@ -10,17 +10,13 @@ var Config Conf // Conf is type of Config type Conf struct { - GitHub struct { - AppID int64 - AppSecret []byte - PEMByte []byte - PEM *rsa.PrivateKey - } + GitHub GitHubApp - MySQLDSN string - Port int - ShoesPluginPath string - RunnerUser string + MySQLDSN string + Port int + ShoesPluginPath string + ShoesPluginOutputPath string + RunnerUser string Debug bool Strict bool // check to registered runner before delete job @@ -33,6 +29,14 @@ type Conf struct { RunnerVersion string } +// GitHubApp is type of config value +type GitHubApp struct { + AppID int64 + AppSecret []byte + PEMByte []byte + PEM *rsa.PrivateKey +} + // Config Environment keys const ( EnvGitHubAppID = "GITHUB_APP_ID" @@ -41,6 +45,7 @@ const ( EnvMySQLURL = "MYSQL_URL" EnvPort = "PORT" EnvShoesPluginPath = "PLUGIN" + EnvShoesPluginOutputPath = "PLUGIN_OUTPUT" EnvRunnerUser = "RUNNER_USER" EnvDebug = "DEBUG" EnvStrict = "STRICT" diff --git a/internal/config/init.go b/pkg/config/init.go similarity index 84% rename from internal/config/init.go rename to pkg/config/init.go index 0f3dc2f..44d066f 100644 --- a/internal/config/init.go +++ b/pkg/config/init.go @@ -21,57 +21,11 @@ import ( func Load() { c := LoadWithDefault() - appID, err := strconv.ParseInt(os.Getenv(EnvGitHubAppID), 10, 64) - if err != nil { - log.Panicf("failed to parse %s: %+v", EnvGitHubAppID, err) - } - c.GitHub.AppID = appID + ga := LoadGitHubApps() + c.GitHub = *ga - pemBase64ed := os.Getenv(EnvGitHubAppPrivateKeyBase64) - if pemBase64ed == "" { - log.Panicf("%s must be set", EnvGitHubAppPrivateKeyBase64) - } - pemByte, err := base64.StdEncoding.DecodeString(pemBase64ed) - if err != nil { - log.Panicf("failed to decode base64 %s: %+v", EnvGitHubAppPrivateKeyBase64, err) - } - c.GitHub.PEMByte = pemByte - block, _ := pem.Decode(pemByte) - if block == nil { - log.Panicf("%s is invalid format, please input private key ", EnvGitHubAppPrivateKeyBase64) - } - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - log.Panicf("%s is invalid format, failed to parse private key: %+v", EnvGitHubAppPrivateKeyBase64, err) - } - c.GitHub.PEM = key - - appSecret := os.Getenv(EnvGitHubAppSecret) - if appSecret == "" { - log.Panicf("%s must be set", EnvGitHubAppSecret) - } - c.GitHub.AppSecret = []byte(appSecret) - - mysqlURL := os.Getenv(EnvMySQLURL) - if mysqlURL == "" { - log.Panicf("%s must be set", EnvMySQLURL) - } - c.MySQLDSN = mysqlURL - - pluginPath := os.Getenv(EnvShoesPluginPath) - if pluginPath == "" { - log.Panicf("%s must be set", EnvShoesPluginPath) - } - fp, err := fetch(pluginPath) - if err != nil { - log.Panicf("failed to fetch plugin binary: %+v", err) - } - absPath, err := checkBinary(fp) - if err != nil { - log.Panicf("failed to check plugin binary: %+v", err) - } - c.ShoesPluginPath = absPath - log.Printf("use plugin path is %s\n", c.ShoesPluginPath) + pluginPath := LoadPluginPath() + c.ShoesPluginPath = pluginPath Config = c } @@ -167,10 +121,80 @@ func LoadWithDefault() Conf { } } + c.ShoesPluginOutputPath = "." + if os.Getenv(EnvShoesPluginOutputPath) != "" { + c.ShoesPluginOutputPath = os.Getenv(EnvShoesPluginOutputPath) + } + Config = c return c } +// LoadGitHubApps load config for GitHub Apps +func LoadGitHubApps() *GitHubApp { + var ga GitHubApp + appID, err := strconv.ParseInt(os.Getenv(EnvGitHubAppID), 10, 64) + if err != nil { + log.Panicf("failed to parse %s: %+v", EnvGitHubAppID, err) + } + ga.AppID = appID + + pemBase64ed := os.Getenv(EnvGitHubAppPrivateKeyBase64) + if pemBase64ed == "" { + log.Panicf("%s must be set", EnvGitHubAppPrivateKeyBase64) + } + pemByte, err := base64.StdEncoding.DecodeString(pemBase64ed) + if err != nil { + log.Panicf("failed to decode base64 %s: %+v", EnvGitHubAppPrivateKeyBase64, err) + } + ga.PEMByte = pemByte + + block, _ := pem.Decode(pemByte) + if block == nil { + log.Panicf("%s is invalid format, please input private key ", EnvGitHubAppPrivateKeyBase64) + } + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + log.Panicf("%s is invalid format, failed to parse private key: %+v", EnvGitHubAppPrivateKeyBase64, err) + } + ga.PEM = key + + appSecret := os.Getenv(EnvGitHubAppSecret) + if appSecret == "" { + log.Panicf("%s must be set", EnvGitHubAppSecret) + } + ga.AppSecret = []byte(appSecret) + + return &ga +} + +// LoadMySQLURL load MySQL URL from environment +func LoadMySQLURL() string { + mysqlURL := os.Getenv(EnvMySQLURL) + if mysqlURL == "" { + log.Panicf("%s must be set", EnvMySQLURL) + } + return mysqlURL +} + +// LoadPluginPath load plugin path from environment +func LoadPluginPath() string { + pluginPath := os.Getenv(EnvShoesPluginPath) + if pluginPath == "" { + log.Panicf("%s must be set", EnvShoesPluginPath) + } + fp, err := fetch(pluginPath) + if err != nil { + log.Panicf("failed to fetch plugin binary: %+v", err) + } + absPath, err := checkBinary(fp) + if err != nil { + log.Panicf("failed to check plugin binary: %+v", err) + } + log.Printf("use plugin path is %s\n", absPath) + return absPath +} + func checkBinary(p string) (string, error) { if _, err := os.Stat(p); err != nil { return "", fmt.Errorf("failed to stat file: %w", err) @@ -210,7 +234,7 @@ func fetch(p string) (string, error) { case "http", "https": return fetchHTTP(u) default: - return "", fmt.Errorf("unsupported fetch schema") + return "", fmt.Errorf("unsupported fetch schema (scheme: %s)", u.Scheme) } } @@ -218,15 +242,19 @@ func fetch(p string) (string, error) { // save to current directory. func fetchHTTP(u *url.URL) (string, error) { log.Printf("fetch plugin binary from %s\n", u.String()) - pwd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to working directory: %w", err) + dir := Config.ShoesPluginOutputPath + if strings.EqualFold(dir, ".") { + pwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to working directory: %w", err) + } + dir = pwd } p := strings.Split(u.Path, "/") fileName := p[len(p)-1] - fp := filepath.Join(pwd, fileName) + fp := filepath.Join(dir, fileName) f, err := os.Create(fp) if err != nil { return "", fmt.Errorf("failed to create os file: %w", err) diff --git a/pkg/datastore/mysql/lock.go b/pkg/datastore/mysql/lock.go index faedc4f..a492307 100644 --- a/pkg/datastore/mysql/lock.go +++ b/pkg/datastore/mysql/lock.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/go-sql-driver/mysql" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" ) diff --git a/pkg/gh/github.go b/pkg/gh/github.go index 38f2cda..ccd73c6 100644 --- a/pkg/gh/github.go +++ b/pkg/gh/github.go @@ -12,7 +12,7 @@ import ( "github.com/google/go-github/v47/github" "github.com/m4ns0ur/httpcache" "github.com/patrickmn/go-cache" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "golang.org/x/oauth2" ) diff --git a/pkg/gh/github_test.go b/pkg/gh/github_test.go index b6fa16d..1b9445e 100644 --- a/pkg/gh/github_test.go +++ b/pkg/gh/github_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" ) func TestDetectScope(t *testing.T) { diff --git a/pkg/gh/jwt.go b/pkg/gh/jwt.go index 29c4049..5ca92d9 100644 --- a/pkg/gh/jwt.go +++ b/pkg/gh/jwt.go @@ -6,8 +6,7 @@ import ( "strings" "time" - "github.com/whywaita/myshoes/internal/config" - + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/logger" "github.com/google/go-github/v47/github" diff --git a/pkg/gh/runner.go b/pkg/gh/runner.go index 925db54..4e62dfc 100644 --- a/pkg/gh/runner.go +++ b/pkg/gh/runner.go @@ -85,7 +85,19 @@ func listRunners(ctx context.Context, client *github.Client, owner, repo string, } // GetLatestRunnerVersion get a latest version of actions/runner -func GetLatestRunnerVersion(ctx context.Context, scope, token string) (string, error) { +func GetLatestRunnerVersion(ctx context.Context, scope string) (string, error) { + clientApps, err := NewClientGitHubApps() + if err != nil { + return "", fmt.Errorf("failed to create a client from Apps: %+v", err) + } + installationID, err := IsInstalledGitHubApp(ctx, scope) + if err != nil { + return "", fmt.Errorf("failed to get installlation id: %w", err) + } + token, _, err := GenerateGitHubAppsToken(ctx, clientApps, installationID, scope) + if err != nil { + return "", fmt.Errorf("failed to get registration token: %w", err) + } client, err := NewClient(token) if err != nil { return "", fmt.Errorf("failed to create GitHub client: %w", err) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 58fad09..781b4ec 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -5,7 +5,7 @@ import ( "os" "sync" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" ) var ( diff --git a/pkg/metric/scrape_memory.go b/pkg/metric/scrape_memory.go index 970cd5b..96d266e 100644 --- a/pkg/metric/scrape_memory.go +++ b/pkg/metric/scrape_memory.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/prometheus/client_golang/prometheus" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/runner" diff --git a/pkg/runner/runner_delete.go b/pkg/runner/runner_delete.go index d0c6fff..4e85cd7 100644 --- a/pkg/runner/runner_delete.go +++ b/pkg/runner/runner_delete.go @@ -9,7 +9,7 @@ import ( "time" "github.com/google/go-github/v47/github" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/logger" diff --git a/pkg/shoes/shoes.go b/pkg/shoes/shoes.go index d9516d9..b6426a6 100644 --- a/pkg/shoes/shoes.go +++ b/pkg/shoes/shoes.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-plugin" pb "github.com/whywaita/myshoes/api/proto.go" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "google.golang.org/grpc" diff --git a/pkg/starter/scripts.go b/pkg/starter/scripts.go index affae49..d6d44e0 100644 --- a/pkg/starter/scripts.go +++ b/pkg/starter/scripts.go @@ -10,9 +10,7 @@ import ( "strings" "text/template" - "github.com/whywaita/myshoes/internal/config" - - "github.com/whywaita/myshoes/pkg/datastore" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/runner" ) @@ -24,8 +22,8 @@ func getPatchedFiles() (string, error) { return runnerService, nil } -func (s *Starter) getSetupScript(ctx context.Context, target datastore.Target, runnerName string) (string, error) { - rawScript, err := s.getSetupRawScript(ctx, target, runnerName) +func (s *Starter) getSetupScript(ctx context.Context, targetScope, runnerName string) (string, error) { + rawScript, err := s.getSetupRawScript(ctx, targetScope, runnerName) if err != nil { return "", fmt.Errorf("failed to get raw setup scripts: %w", err) } @@ -46,13 +44,13 @@ func (s *Starter) getSetupScript(ctx context.Context, target datastore.Target, r return fmt.Sprintf(templateCompressedScript, encoded), nil } -func (s *Starter) getSetupRawScript(ctx context.Context, target datastore.Target, runnerName string) (string, error) { +func (s *Starter) getSetupRawScript(ctx context.Context, targetScope, runnerName string) (string, error) { runnerUser := config.Config.RunnerUser githubURL := config.Config.GitHubURL targetRunnerVersion := s.runnerVersion if strings.EqualFold(s.runnerVersion, "latest") { - latestVersion, err := gh.GetLatestRunnerVersion(ctx, target.Scope, target.GitHubToken) + latestVersion, err := gh.GetLatestRunnerVersion(ctx, targetScope) if err != nil { return "", fmt.Errorf("failed to get latest version of actions/runner: %w", err) } @@ -69,11 +67,11 @@ func (s *Starter) getSetupRawScript(ctx context.Context, target datastore.Target return "", fmt.Errorf("failed to get patched files: %w", err) } - installationID, err := gh.IsInstalledGitHubApp(ctx, target.Scope) + installationID, err := gh.IsInstalledGitHubApp(ctx, targetScope) if err != nil { return "", fmt.Errorf("failed to get installlation id: %w", err) } - token, err := gh.GetRunnerRegistrationToken(ctx, installationID, target.Scope) + token, err := gh.GetRunnerRegistrationToken(ctx, installationID, targetScope) if err != nil { return "", fmt.Errorf("failed to generate runner register token: %w", err) } @@ -84,7 +82,7 @@ func (s *Starter) getSetupRawScript(ctx context.Context, target datastore.Target } v := templateCreateLatestRunnerOnceValue{ - Scope: target.Scope, + Scope: targetScope, GHEDomain: config.Config.GitHubURL, RunnerRegistrationToken: token, RunnerName: runnerName, diff --git a/pkg/starter/starter.go b/pkg/starter/starter.go index f2de641..9a3ccac 100644 --- a/pkg/starter/starter.go +++ b/pkg/starter/starter.go @@ -17,7 +17,7 @@ import ( "github.com/google/go-github/v47/github" uuid "github.com/satori/go.uuid" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/logger" @@ -158,7 +158,7 @@ func (s *Starter) run(ctx context.Context, ch chan datastore.Job) error { CountRunning.Add(-1) }() - if err := s.processJob(ctx, job); err != nil { + if err := s.ProcessJob(ctx, job); err != nil { logger.Logf(false, "failed to process job: %+v\n", err) } }(job) @@ -169,7 +169,8 @@ func (s *Starter) run(ctx context.Context, ch chan datastore.Job) error { } } -func (s *Starter) processJob(ctx context.Context, job datastore.Job) error { +// ProcessJob is process job +func (s *Starter) ProcessJob(ctx context.Context, job datastore.Job) error { logger.Logf(false, "start job (job id: %s)\n", job.UUID.String()) isOK, err := s.safety.Check(&job) @@ -268,7 +269,8 @@ func (s *Starter) bung(ctx context.Context, job datastore.Job, target datastore. logger.Logf(false, "start create instance (job: %s)", job.UUID) runnerName := runner.ToName(job.UUID.String()) - script, err := s.getSetupScript(ctx, target, runnerName) + targetScope := getTargetScope(target, job) + script, err := s.getSetupScript(ctx, targetScope, runnerName) if err != nil { return "", "", "", datastore.ResourceTypeUnknown, fmt.Errorf("failed to get setup scripts: %w", err) } @@ -294,6 +296,15 @@ func (s *Starter) bung(ctx context.Context, job datastore.Job, target datastore. return cloudID, ipAddress, shoesType, resourceType, nil } +// getTargetScope from target, but receive from job if datastore.target.Scope is empty +// this function is for datastore that don't store target. +func getTargetScope(target datastore.Target, job datastore.Job) string { + if target.Scope == "" { + return job.Repository + } + return target.Scope +} + func deleteInstance(ctx context.Context, cloudID, checkEventJSON string) error { client, teardown, err := shoes.GetClient() if err != nil { diff --git a/pkg/web/config.go b/pkg/web/config.go index b8afd1f..4009d27 100644 --- a/pkg/web/config.go +++ b/pkg/web/config.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/logger" ) diff --git a/pkg/web/http.go b/pkg/web/http.go index 9f661d0..8251d93 100644 --- a/pkg/web/http.go +++ b/pkg/web/http.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/whywaita/myshoes/internal/config" + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/logger" @@ -34,7 +34,7 @@ func NewMux(ds datastore.Datastore) *goji.Mux { mux.HandleFunc(pat.Post("/github/events"), func(w http.ResponseWriter, r *http.Request) { apacheLogging(r) - handleGitHubEvent(w, r, ds) + HandleGitHubEvent(w, r, ds) }) // REST API for targets @@ -72,7 +72,7 @@ func NewMux(ds datastore.Datastore) *goji.Mux { // metrics endpoint mux.HandleFunc(pat.Get("/metrics"), func(w http.ResponseWriter, r *http.Request) { apacheLogging(r) - handleMetrics(w, r, ds) + HandleMetrics(w, r, ds) }) return mux diff --git a/pkg/web/metrics.go b/pkg/web/metrics.go index 13f51c3..77b9df1 100644 --- a/pkg/web/metrics.go +++ b/pkg/web/metrics.go @@ -11,7 +11,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) -func handleMetrics(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) { +// HandleMetrics handle metrics endpoint +func HandleMetrics(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) { ctx := r.Context() registry := prometheus.NewRegistry() diff --git a/pkg/web/target_create.go b/pkg/web/target_create.go index 8db9e7a..0fd57a4 100644 --- a/pkg/web/target_create.go +++ b/pkg/web/target_create.go @@ -9,9 +9,9 @@ import ( "net/http" "time" - "github.com/whywaita/myshoes/internal/config" - uuid "github.com/satori/go.uuid" + + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/logger" diff --git a/pkg/web/webhook.go b/pkg/web/webhook.go index 97f9ed3..eb36e7d 100644 --- a/pkg/web/webhook.go +++ b/pkg/web/webhook.go @@ -11,13 +11,15 @@ import ( "github.com/google/go-github/v47/github" uuid "github.com/satori/go.uuid" - "github.com/whywaita/myshoes/internal/config" + + "github.com/whywaita/myshoes/pkg/config" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/logger" ) -func handleGitHubEvent(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) { +// HandleGitHubEvent handle GitHub webhook event +func HandleGitHubEvent(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) { ctx := r.Context() payload, err := github.ValidatePayload(r, config.Config.GitHub.AppSecret)