Showing preview only (375K chars total). Download the full file or copy to clipboard to get everything.
Repository: whywaita/myshoes
Branch: master
Commit: d20faef23bff
Files: 93
Total size: 350.2 KB
Directory structure:
gitextract_98bm4mpl/
├── .github/
│ └── workflows/
│ ├── build-docker-sha.yaml
│ ├── release.yaml
│ └── test.yaml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api/
│ ├── myshoes/
│ │ ├── README.md
│ │ ├── client.go
│ │ ├── http.go
│ │ └── target.go
│ ├── proto/
│ │ └── myshoes.proto
│ └── proto.go/
│ ├── myshoes.pb.go
│ └── myshoes_grpc.pb.go
├── cmd/
│ ├── server/
│ │ └── cmd.go
│ └── shoes-tester/
│ └── main.go
├── docs/
│ ├── 01_01_for_admin_setup.md
│ ├── 01_02_for_admin_tips.md
│ ├── 02_01_for_user_setup.md
│ ├── 03_how-to-develop-shoes.md
│ └── assets/
│ └── myshoes.service
├── go.mod
├── go.sum
├── internal/
│ ├── testutils/
│ │ ├── mysql.go
│ │ ├── testutils.go
│ │ └── web.go
│ └── util/
│ └── util.go
└── pkg/
├── config/
│ ├── config.go
│ └── init.go
├── datastore/
│ ├── github.go
│ ├── interface.go
│ ├── memory/
│ │ └── memory.go
│ ├── mysql/
│ │ ├── job.go
│ │ ├── job_test.go
│ │ ├── lock.go
│ │ ├── mysql.go
│ │ ├── mysql_test.go
│ │ ├── runner.go
│ │ ├── runner_test.go
│ │ ├── schema.sql
│ │ ├── target.go
│ │ └── target_test.go
│ └── resource_type.go
├── docker/
│ └── ratelimit.go
├── gh/
│ ├── github.go
│ ├── github_test.go
│ ├── installation.go
│ ├── jwt.go
│ ├── jwt_test.go
│ ├── label.go
│ ├── metrics.go
│ ├── metrics_test.go
│ ├── ratelimit.go
│ ├── runner.go
│ ├── scope.go
│ ├── token_registration.go
│ ├── webhook.go
│ ├── workflow_job.go
│ └── workflow_run.go
├── logger/
│ └── logger.go
├── metric/
│ ├── collector.go
│ ├── scrape_datastore.go
│ ├── scrape_github.go
│ ├── scrape_memory.go
│ └── webhook.go
├── runner/
│ ├── metrics.go
│ ├── runner.go
│ ├── runner_delete.go
│ ├── runner_delete_ephemeral.go
│ ├── runner_delete_once.go
│ ├── token_update.go
│ └── util.go
├── shoes/
│ └── shoes.go
├── starter/
│ ├── README.md
│ ├── error.go
│ ├── metric.go
│ ├── metrics.go
│ ├── safety/
│ │ ├── README.md
│ │ ├── safety.go
│ │ └── unlimited/
│ │ └── unlimited.go
│ ├── scripts/
│ │ └── RunnerService.js
│ ├── scripts.go
│ └── starter.go
└── web/
├── config.go
├── http.go
├── http_test.go
├── metrics.go
├── target.go
├── target_create.go
├── target_test.go
└── webhook.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build-docker-sha.yaml
================================================
name: Build Docker image (sha)
on:
push:
branches:
- "**"
workflow_dispatch:
jobs:
docker-build-sha:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Cache Docker layers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/myshoes
tags: |
type=sha
- name: Build container image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
================================================
FILE: .github/workflows/release.yaml
================================================
name: release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version-file: 'go.mod'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
id: meta
with:
images: ghcr.io/whywaita/myshoes
tags: |
type=raw,value=latest
type=semver,pattern={{raw}}
type=sha
- name: Build container image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
================================================
FILE: .github/workflows/test.yaml
================================================
name: test
on:
push:
branches:
- "**"
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 1
- name: setup go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version-file: 'go.mod'
- name: lint
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
- name: vet
run: |
go vet ./...
- name: test
run: |
make test
docker-build-test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 1
- name: Build container image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: false
tags: ${{ steps.meta.outputs.tags }}
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/macos,intellij,go
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,intellij,go
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
### Go Patch ###
/vendor/
/Godeps/
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# End of https://www.toptal.com/developers/gitignore/api/macos,intellij,go
/myshoes*
================================================
FILE: .goreleaser.yml
================================================
builds:
- main: ./cmd/server/cmd.go
goos:
- linux
goarch:
- amd64
- arm64
================================================
FILE: Dockerfile
================================================
FROM golang:1.25 AS builder
WORKDIR /go/src/github.com/whywaita/myshoes
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
RUN apt-get update -y \
&& apt-get install -y protobuf-compiler
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
COPY . .
RUN make build-linux
FROM alpine
RUN apk update \
&& apk update
RUN apk add --no-cache ca-certificates \
&& update-ca-certificates 2>/dev/null || true
COPY --from=builder /go/src/github.com/whywaita/myshoes/myshoes-linux-amd64 /app
CMD ["/app"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 whywaita
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: help
.DEFAULT_GOAL := help
CURRENT_REVISION = $(shell git rev-parse --short HEAD)
BUILD_LDFLAGS = "-X main.revision=$(CURRENT_REVISION)"
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
build: ## Build All
go generate ./...
make build-proto
go build -o myshoes -ldflags $(BUILD_LDFLAGS) cmd/server/cmd.go
build-linux: ## Build for Linux
go generate ./...
make build-proto
GOOS=linux GOARCH=amd64 go build -o myshoes-linux-amd64 -ldflags $(BUILD_LDFLAGS) cmd/server/cmd.go
build-proto: ## Build proto file
mkdir -p tmp/proto-go
rm -rf api/proto.go
protoc -I=api/proto/ --go_out=tmp/proto-go/ --go-grpc_out=tmp/proto-go/ api/proto/**.proto
mv tmp/proto-go/github.com/whywaita/myshoes/api/proto.go api/
rm -rf tmp
test: ## Exec test
go test -v ./...
================================================
FILE: README.md
================================================
# myshoes: Auto scaling self-hosted runner for GitHub Actions

[](https://github.com/jonico/awesome-runners)
[](https://pkg.go.dev/github.com/whywaita/myshoes)
[](https://github.com/whywaita/myshoes/actions/workflows/test.yaml)
[](LICENSE)
[](https://goreportcard.com/report/github.com/whywaita/myshoes)
Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions!
## Features
- Auto-scaling and runner with your cloud-provider
- your infrastructure (private cloud, homelab...)
- [LXD](https://linuxcontainers.org): [shoes-lxd](https://github.com/whywaita/myshoes-providers/tree/master/shoes-lxd)
- [OpenStack](https://www.openstack.org): [shoes-openstack](https://github.com/whywaita/myshoes-providers/tree/master/shoes-openstack)
- a low-cost instance in public cloud
- [AWS EC2 Spot Instances](https://aws.amazon.com/ec2/spot): [shoes-aws](https://github.com/whywaita/myshoes-providers/tree/master/shoes-aws)
- [GCP Preemptible VM instances](https://cloud.google.com/compute/docs/instances/preemptible): shoes-gcp (not yet)
- using special hardware
- Graphics Processing Unit (GPU)
- Field Programmable Gate Array (FPGA)
- And more in [whywaita/myshoes-providers](https://github.com/whywaita/myshoes-providers)
## Setup (only once)
Please see [Documents](./docs).
## How to contribute
1. Fork it
1. Clone original repository `git clone https://github.com/whywaita/myshoes`
1. Add remote your repository `git remote add your-name https://github.com/${your-name}/myshoes`
1. Create your feature branch `git switch -c my-new-feature`
1. Commit your changes `git commit -am 'Add some feature'`
1. Push to the branch `git push your-name my-new-feature`
1. Create new Pull Request
## Publications
### Talk
- [Development myshoes and Provide Cycloud-hosted runner -- GitHub Actions with your shoes. (en)](https://www.slideshare.net/whywaita/development-myshoes-and-provide-cycloudhosted-runner-github-actions-with-your-shoes)
- [Development OSS CI/CD platform in CyberAgent (ja)](https://www.slideshare.net/whywaita/cyberagent-oss-cicd-myshoes-cicd2021)
================================================
FILE: api/myshoes/README.md
================================================
# myshoes-sdk-go
The Go SDK for myshoes
## Usage
```go
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"github.com/whywaita/myshoes/api/myshoes"
)
func main() {
// Set customized HTTP Client
customHTTPClient := http.DefaultClient
// Set customized logger
customLogger := log.New(io.Discard, "", log.LstdFlags)
client, err := myshoes.NewClient("https://example.com", customHTTPClient, customLogger)
if err != nil {
// ...
}
targets, err := client.ListTarget(context.Background())
if err != nil {
// ...
}
fmt.Println(targets)
}
```
================================================
FILE: api/myshoes/client.go
================================================
package myshoes
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"path"
"strings"
)
// Client is a client for myshoes
type Client struct {
HTTPClient http.Client
URL *url.URL
UserAgent string
Logger *log.Logger
}
const (
defaultUserAgent = "myshoes-sdk-go"
)
// NewClient create a Client
func NewClient(endpoint string, client *http.Client, logger *log.Logger) (*Client, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
}
httpClient := client
if httpClient == nil {
httpClient = http.DefaultClient
}
l := logger
if l == nil {
return &Client{
HTTPClient: *httpClient,
URL: u,
// Default is discard logger
Logger: log.New(io.Discard, "", log.LstdFlags),
}, nil
}
return &Client{
HTTPClient: *httpClient,
URL: u,
Logger: l,
}, nil
}
func (c *Client) newRequest(ctx context.Context, method, spath string, body io.Reader) (*http.Request, error) {
u := *c.URL
u.Path = path.Join(c.URL.Path, spath)
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, fmt.Errorf("failed to create a new HTTP request: %w", err)
}
ua := c.UserAgent
if strings.EqualFold(ua, "") {
ua = defaultUserAgent
}
req.Header.Set("User-Agent", ua)
req.Header.Set("Content-Type", "application/json")
return req, nil
}
// Error values
var (
errCreateRequest = "failed to create request: %w"
errRequest = "failed to request: %w"
errDecodeBody = "failed to decodeBody: %w"
)
================================================
FILE: api/myshoes/http.go
================================================
package myshoes
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/whywaita/myshoes/pkg/web"
)
func decodeBody(resp *http.Response, out interface{}) error {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to io.ReadAll(resp.Body): %w", err)
}
if err := json.Unmarshal(body, out); err != nil {
return fmt.Errorf("failed to json.Unmarshal() (out: %s): %w", body, err)
}
return nil
}
func decodeErrorBody(resp *http.Response) error {
var e web.ErrorResponse
if err := decodeBody(resp, &e); err != nil {
return fmt.Errorf(errDecodeBody, err)
}
return fmt.Errorf("%s", e.Error)
}
func (c *Client) request(req *http.Request, out interface{}) error {
c.Logger.Printf("Do request: %+v", req)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("failed to do HTTP request: %w", err)
}
switch {
case resp.StatusCode == http.StatusNoContent:
return nil
case resp.StatusCode >= 400:
return decodeErrorBody(resp)
}
if err := decodeBody(resp, out); err != nil {
return fmt.Errorf(errDecodeBody, err)
}
return nil
}
================================================
FILE: api/myshoes/target.go
================================================
package myshoes
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/whywaita/myshoes/pkg/web"
)
// CreateTarget create a target
func (c *Client) CreateTarget(ctx context.Context, param web.TargetCreateParam) (*web.UserTarget, error) {
spath := "/target"
jb, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("failed to json.Marshal: %w", err)
}
req, err := c.newRequest(ctx, http.MethodPost, spath, bytes.NewBuffer(jb))
if err != nil {
return nil, fmt.Errorf(errCreateRequest, err)
}
var target web.UserTarget
if err := c.request(req, &target); err != nil {
return nil, fmt.Errorf(errRequest, err)
}
return &target, nil
}
// GetTarget get a target
func (c *Client) GetTarget(ctx context.Context, targetID string) (*web.UserTarget, error) {
spath := fmt.Sprintf("/target/%s", targetID)
req, err := c.newRequest(ctx, http.MethodGet, spath, nil)
if err != nil {
return nil, fmt.Errorf(errCreateRequest, err)
}
var target web.UserTarget
if err := c.request(req, &target); err != nil {
return nil, fmt.Errorf(errRequest, err)
}
return &target, nil
}
// UpdateTarget update a target
func (c *Client) UpdateTarget(ctx context.Context, targetID string, param web.TargetCreateParam) (*web.UserTarget, error) {
spath := fmt.Sprintf("/target/%s", targetID)
jb, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("failed to json.Marshal: %w", err)
}
req, err := c.newRequest(ctx, http.MethodPost, spath, bytes.NewBuffer(jb))
if err != nil {
return nil, fmt.Errorf(errCreateRequest, err)
}
var target web.UserTarget
if err := c.request(req, &target); err != nil {
return nil, fmt.Errorf(errRequest, err)
}
return &target, nil
}
// DeleteTarget delete a target
func (c *Client) DeleteTarget(ctx context.Context, targetID string) error {
spath := fmt.Sprintf("/target/%s", targetID)
req, err := c.newRequest(ctx, http.MethodDelete, spath, nil)
if err != nil {
return fmt.Errorf(errCreateRequest, err)
}
var i interface{} // this endpoint return N/A
if err := c.request(req, &i); err != nil {
return fmt.Errorf(errRequest, err)
}
return nil
}
// ListTarget get a list of target
func (c *Client) ListTarget(ctx context.Context) ([]web.UserTarget, error) {
spath := "/target"
req, err := c.newRequest(ctx, http.MethodGet, spath, nil)
if err != nil {
return nil, fmt.Errorf(errCreateRequest, err)
}
var targets []web.UserTarget
if err := c.request(req, &targets); err != nil {
return nil, fmt.Errorf(errRequest, err)
}
return targets, nil
}
================================================
FILE: api/proto/myshoes.proto
================================================
syntax = "proto3";
package whywaita.myshoes;
option go_package = "github.com/whywaita/myshoes/api/proto.go";
service Shoes {
rpc AddInstance(AddInstanceRequest) returns (AddInstanceResponse) {}
rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) {}
}
enum ResourceType {
Unknown = 0;
Nano = 1;
Micro = 2;
Small = 3;
Medium = 4;
Large = 5;
XLarge = 6;
XLarge2 = 7;
XLarge3 = 8;
XLarge4 = 9;
}
message AddInstanceRequest {
string runner_name = 1;
string setup_script = 2;
ResourceType resource_type = 3;
repeated string labels = 4;
}
message AddInstanceResponse {
string cloud_id = 1;
string shoes_type = 2;
string ip_address = 3;
ResourceType resource_type = 4;
}
message DeleteInstanceRequest {
string cloud_id = 1;
repeated string labels = 2;
}
message DeleteInstanceResponse {}
================================================
FILE: api/proto.go/myshoes.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v5.29.3
// source: myshoes.proto
package proto_go
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ResourceType int32
const (
ResourceType_Unknown ResourceType = 0
ResourceType_Nano ResourceType = 1
ResourceType_Micro ResourceType = 2
ResourceType_Small ResourceType = 3
ResourceType_Medium ResourceType = 4
ResourceType_Large ResourceType = 5
ResourceType_XLarge ResourceType = 6
ResourceType_XLarge2 ResourceType = 7
ResourceType_XLarge3 ResourceType = 8
ResourceType_XLarge4 ResourceType = 9
)
// Enum value maps for ResourceType.
var (
ResourceType_name = map[int32]string{
0: "Unknown",
1: "Nano",
2: "Micro",
3: "Small",
4: "Medium",
5: "Large",
6: "XLarge",
7: "XLarge2",
8: "XLarge3",
9: "XLarge4",
}
ResourceType_value = map[string]int32{
"Unknown": 0,
"Nano": 1,
"Micro": 2,
"Small": 3,
"Medium": 4,
"Large": 5,
"XLarge": 6,
"XLarge2": 7,
"XLarge3": 8,
"XLarge4": 9,
}
)
func (x ResourceType) Enum() *ResourceType {
p := new(ResourceType)
*p = x
return p
}
func (x ResourceType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ResourceType) Descriptor() protoreflect.EnumDescriptor {
return file_myshoes_proto_enumTypes[0].Descriptor()
}
func (ResourceType) Type() protoreflect.EnumType {
return &file_myshoes_proto_enumTypes[0]
}
func (x ResourceType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ResourceType.Descriptor instead.
func (ResourceType) EnumDescriptor() ([]byte, []int) {
return file_myshoes_proto_rawDescGZIP(), []int{0}
}
type AddInstanceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RunnerName string `protobuf:"bytes,1,opt,name=runner_name,json=runnerName,proto3" json:"runner_name,omitempty"`
SetupScript string `protobuf:"bytes,2,opt,name=setup_script,json=setupScript,proto3" json:"setup_script,omitempty"`
ResourceType ResourceType `protobuf:"varint,3,opt,name=resource_type,json=resourceType,proto3,enum=whywaita.myshoes.ResourceType" json:"resource_type,omitempty"`
Labels []string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddInstanceRequest) Reset() {
*x = AddInstanceRequest{}
mi := &file_myshoes_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddInstanceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddInstanceRequest) ProtoMessage() {}
func (x *AddInstanceRequest) ProtoReflect() protoreflect.Message {
mi := &file_myshoes_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddInstanceRequest.ProtoReflect.Descriptor instead.
func (*AddInstanceRequest) Descriptor() ([]byte, []int) {
return file_myshoes_proto_rawDescGZIP(), []int{0}
}
func (x *AddInstanceRequest) GetRunnerName() string {
if x != nil {
return x.RunnerName
}
return ""
}
func (x *AddInstanceRequest) GetSetupScript() string {
if x != nil {
return x.SetupScript
}
return ""
}
func (x *AddInstanceRequest) GetResourceType() ResourceType {
if x != nil {
return x.ResourceType
}
return ResourceType_Unknown
}
func (x *AddInstanceRequest) GetLabels() []string {
if x != nil {
return x.Labels
}
return nil
}
type AddInstanceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
CloudId string `protobuf:"bytes,1,opt,name=cloud_id,json=cloudId,proto3" json:"cloud_id,omitempty"`
ShoesType string `protobuf:"bytes,2,opt,name=shoes_type,json=shoesType,proto3" json:"shoes_type,omitempty"`
IpAddress string `protobuf:"bytes,3,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"`
ResourceType ResourceType `protobuf:"varint,4,opt,name=resource_type,json=resourceType,proto3,enum=whywaita.myshoes.ResourceType" json:"resource_type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AddInstanceResponse) Reset() {
*x = AddInstanceResponse{}
mi := &file_myshoes_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AddInstanceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AddInstanceResponse) ProtoMessage() {}
func (x *AddInstanceResponse) ProtoReflect() protoreflect.Message {
mi := &file_myshoes_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AddInstanceResponse.ProtoReflect.Descriptor instead.
func (*AddInstanceResponse) Descriptor() ([]byte, []int) {
return file_myshoes_proto_rawDescGZIP(), []int{1}
}
func (x *AddInstanceResponse) GetCloudId() string {
if x != nil {
return x.CloudId
}
return ""
}
func (x *AddInstanceResponse) GetShoesType() string {
if x != nil {
return x.ShoesType
}
return ""
}
func (x *AddInstanceResponse) GetIpAddress() string {
if x != nil {
return x.IpAddress
}
return ""
}
func (x *AddInstanceResponse) GetResourceType() ResourceType {
if x != nil {
return x.ResourceType
}
return ResourceType_Unknown
}
type DeleteInstanceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
CloudId string `protobuf:"bytes,1,opt,name=cloud_id,json=cloudId,proto3" json:"cloud_id,omitempty"`
Labels []string `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteInstanceRequest) Reset() {
*x = DeleteInstanceRequest{}
mi := &file_myshoes_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteInstanceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteInstanceRequest) ProtoMessage() {}
func (x *DeleteInstanceRequest) ProtoReflect() protoreflect.Message {
mi := &file_myshoes_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteInstanceRequest.ProtoReflect.Descriptor instead.
func (*DeleteInstanceRequest) Descriptor() ([]byte, []int) {
return file_myshoes_proto_rawDescGZIP(), []int{2}
}
func (x *DeleteInstanceRequest) GetCloudId() string {
if x != nil {
return x.CloudId
}
return ""
}
func (x *DeleteInstanceRequest) GetLabels() []string {
if x != nil {
return x.Labels
}
return nil
}
type DeleteInstanceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteInstanceResponse) Reset() {
*x = DeleteInstanceResponse{}
mi := &file_myshoes_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteInstanceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteInstanceResponse) ProtoMessage() {}
func (x *DeleteInstanceResponse) ProtoReflect() protoreflect.Message {
mi := &file_myshoes_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteInstanceResponse.ProtoReflect.Descriptor instead.
func (*DeleteInstanceResponse) Descriptor() ([]byte, []int) {
return file_myshoes_proto_rawDescGZIP(), []int{3}
}
var File_myshoes_proto protoreflect.FileDescriptor
const file_myshoes_proto_rawDesc = "" +
"\n" +
"\rmyshoes.proto\x12\x10whywaita.myshoes\"\xb5\x01\n" +
"\x12AddInstanceRequest\x12\x1f\n" +
"\vrunner_name\x18\x01 \x01(\tR\n" +
"runnerName\x12!\n" +
"\fsetup_script\x18\x02 \x01(\tR\vsetupScript\x12C\n" +
"\rresource_type\x18\x03 \x01(\x0e2\x1e.whywaita.myshoes.ResourceTypeR\fresourceType\x12\x16\n" +
"\x06labels\x18\x04 \x03(\tR\x06labels\"\xb3\x01\n" +
"\x13AddInstanceResponse\x12\x19\n" +
"\bcloud_id\x18\x01 \x01(\tR\acloudId\x12\x1d\n" +
"\n" +
"shoes_type\x18\x02 \x01(\tR\tshoesType\x12\x1d\n" +
"\n" +
"ip_address\x18\x03 \x01(\tR\tipAddress\x12C\n" +
"\rresource_type\x18\x04 \x01(\x0e2\x1e.whywaita.myshoes.ResourceTypeR\fresourceType\"J\n" +
"\x15DeleteInstanceRequest\x12\x19\n" +
"\bcloud_id\x18\x01 \x01(\tR\acloudId\x12\x16\n" +
"\x06labels\x18\x02 \x03(\tR\x06labels\"\x18\n" +
"\x16DeleteInstanceResponse*\x85\x01\n" +
"\fResourceType\x12\v\n" +
"\aUnknown\x10\x00\x12\b\n" +
"\x04Nano\x10\x01\x12\t\n" +
"\x05Micro\x10\x02\x12\t\n" +
"\x05Small\x10\x03\x12\n" +
"\n" +
"\x06Medium\x10\x04\x12\t\n" +
"\x05Large\x10\x05\x12\n" +
"\n" +
"\x06XLarge\x10\x06\x12\v\n" +
"\aXLarge2\x10\a\x12\v\n" +
"\aXLarge3\x10\b\x12\v\n" +
"\aXLarge4\x10\t2\xcc\x01\n" +
"\x05Shoes\x12\\\n" +
"\vAddInstance\x12$.whywaita.myshoes.AddInstanceRequest\x1a%.whywaita.myshoes.AddInstanceResponse\"\x00\x12e\n" +
"\x0eDeleteInstance\x12'.whywaita.myshoes.DeleteInstanceRequest\x1a(.whywaita.myshoes.DeleteInstanceResponse\"\x00B*Z(github.com/whywaita/myshoes/api/proto.gob\x06proto3"
var (
file_myshoes_proto_rawDescOnce sync.Once
file_myshoes_proto_rawDescData []byte
)
func file_myshoes_proto_rawDescGZIP() []byte {
file_myshoes_proto_rawDescOnce.Do(func() {
file_myshoes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_myshoes_proto_rawDesc), len(file_myshoes_proto_rawDesc)))
})
return file_myshoes_proto_rawDescData
}
var file_myshoes_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_myshoes_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_myshoes_proto_goTypes = []any{
(ResourceType)(0), // 0: whywaita.myshoes.ResourceType
(*AddInstanceRequest)(nil), // 1: whywaita.myshoes.AddInstanceRequest
(*AddInstanceResponse)(nil), // 2: whywaita.myshoes.AddInstanceResponse
(*DeleteInstanceRequest)(nil), // 3: whywaita.myshoes.DeleteInstanceRequest
(*DeleteInstanceResponse)(nil), // 4: whywaita.myshoes.DeleteInstanceResponse
}
var file_myshoes_proto_depIdxs = []int32{
0, // 0: whywaita.myshoes.AddInstanceRequest.resource_type:type_name -> whywaita.myshoes.ResourceType
0, // 1: whywaita.myshoes.AddInstanceResponse.resource_type:type_name -> whywaita.myshoes.ResourceType
1, // 2: whywaita.myshoes.Shoes.AddInstance:input_type -> whywaita.myshoes.AddInstanceRequest
3, // 3: whywaita.myshoes.Shoes.DeleteInstance:input_type -> whywaita.myshoes.DeleteInstanceRequest
2, // 4: whywaita.myshoes.Shoes.AddInstance:output_type -> whywaita.myshoes.AddInstanceResponse
4, // 5: whywaita.myshoes.Shoes.DeleteInstance:output_type -> whywaita.myshoes.DeleteInstanceResponse
4, // [4:6] is the sub-list for method output_type
2, // [2:4] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_myshoes_proto_init() }
func file_myshoes_proto_init() {
if File_myshoes_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_myshoes_proto_rawDesc), len(file_myshoes_proto_rawDesc)),
NumEnums: 1,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_myshoes_proto_goTypes,
DependencyIndexes: file_myshoes_proto_depIdxs,
EnumInfos: file_myshoes_proto_enumTypes,
MessageInfos: file_myshoes_proto_msgTypes,
}.Build()
File_myshoes_proto = out.File
file_myshoes_proto_goTypes = nil
file_myshoes_proto_depIdxs = nil
}
================================================
FILE: api/proto.go/myshoes_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.0
// - protoc v5.29.3
// source: myshoes.proto
package proto_go
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Shoes_AddInstance_FullMethodName = "/whywaita.myshoes.Shoes/AddInstance"
Shoes_DeleteInstance_FullMethodName = "/whywaita.myshoes.Shoes/DeleteInstance"
)
// ShoesClient is the client API for Shoes service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ShoesClient interface {
AddInstance(ctx context.Context, in *AddInstanceRequest, opts ...grpc.CallOption) (*AddInstanceResponse, error)
DeleteInstance(ctx context.Context, in *DeleteInstanceRequest, opts ...grpc.CallOption) (*DeleteInstanceResponse, error)
}
type shoesClient struct {
cc grpc.ClientConnInterface
}
func NewShoesClient(cc grpc.ClientConnInterface) ShoesClient {
return &shoesClient{cc}
}
func (c *shoesClient) AddInstance(ctx context.Context, in *AddInstanceRequest, opts ...grpc.CallOption) (*AddInstanceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AddInstanceResponse)
err := c.cc.Invoke(ctx, Shoes_AddInstance_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *shoesClient) DeleteInstance(ctx context.Context, in *DeleteInstanceRequest, opts ...grpc.CallOption) (*DeleteInstanceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteInstanceResponse)
err := c.cc.Invoke(ctx, Shoes_DeleteInstance_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ShoesServer is the server API for Shoes service.
// All implementations must embed UnimplementedShoesServer
// for forward compatibility.
type ShoesServer interface {
AddInstance(context.Context, *AddInstanceRequest) (*AddInstanceResponse, error)
DeleteInstance(context.Context, *DeleteInstanceRequest) (*DeleteInstanceResponse, error)
mustEmbedUnimplementedShoesServer()
}
// UnimplementedShoesServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedShoesServer struct{}
func (UnimplementedShoesServer) AddInstance(context.Context, *AddInstanceRequest) (*AddInstanceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AddInstance not implemented")
}
func (UnimplementedShoesServer) DeleteInstance(context.Context, *DeleteInstanceRequest) (*DeleteInstanceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteInstance not implemented")
}
func (UnimplementedShoesServer) mustEmbedUnimplementedShoesServer() {}
func (UnimplementedShoesServer) testEmbeddedByValue() {}
// UnsafeShoesServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ShoesServer will
// result in compilation errors.
type UnsafeShoesServer interface {
mustEmbedUnimplementedShoesServer()
}
func RegisterShoesServer(s grpc.ServiceRegistrar, srv ShoesServer) {
// If the following call panics, it indicates UnimplementedShoesServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Shoes_ServiceDesc, srv)
}
func _Shoes_AddInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddInstanceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ShoesServer).AddInstance(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Shoes_AddInstance_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ShoesServer).AddInstance(ctx, req.(*AddInstanceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Shoes_DeleteInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteInstanceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ShoesServer).DeleteInstance(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Shoes_DeleteInstance_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ShoesServer).DeleteInstance(ctx, req.(*DeleteInstanceRequest))
}
return interceptor(ctx, in, info, handler)
}
// Shoes_ServiceDesc is the grpc.ServiceDesc for Shoes service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Shoes_ServiceDesc = grpc.ServiceDesc{
ServiceName: "whywaita.myshoes.Shoes",
HandlerType: (*ShoesServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "AddInstance",
Handler: _Shoes_AddInstance_Handler,
},
{
MethodName: "DeleteInstance",
Handler: _Shoes_DeleteInstance_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "myshoes.proto",
}
================================================
FILE: cmd/server/cmd.go
================================================
package main
import (
"context"
"fmt"
"log"
"net/http"
"runtime"
"strings"
"time"
"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"
"github.com/whywaita/myshoes/pkg/logger"
"github.com/whywaita/myshoes/pkg/runner"
"github.com/whywaita/myshoes/pkg/starter"
"github.com/whywaita/myshoes/pkg/starter/safety/unlimited"
"github.com/whywaita/myshoes/pkg/web"
"golang.org/x/sync/errgroup"
)
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)
}
}
func main() {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
go func() {
log.Fatal(http.ListenAndServe("localhost:6060", nil))
}()
myshoes, err := newShoes()
if err != nil {
log.Fatalln(err)
}
if err := myshoes.Run(); err != nil {
log.Fatalln(err)
}
}
type myShoes struct {
ds datastore.Datastore
start *starter.Starter
run *runner.Manager
}
// newShoes create myshoes.
func newShoes() (*myShoes, error) {
notifyEnqueueCh := make(chan struct{}, 1)
ds, err := mysql.New(config.Config.MySQLDSN, notifyEnqueueCh)
if err != nil {
return nil, fmt.Errorf("failed to mysql.New: %w", err)
}
unlimit := unlimited.Unlimited{}
s := starter.New(ds, unlimit, config.Config.RunnerVersion, notifyEnqueueCh)
manager := runner.New(ds, config.Config.RunnerVersion)
return &myShoes{
ds: ds,
start: s,
run: manager,
}, nil
}
// Run start services.
func (m *myShoes) Run() error {
eg, ctx := errgroup.WithContext(context.Background())
for {
logger.Logf(false, "start getting lock...")
isLocked, err := m.ds.IsLocked(ctx)
if err != nil {
return fmt.Errorf("failed to check lock: %w", err)
}
if strings.EqualFold(isLocked, datastore.IsNotLocked) {
if err := m.ds.GetLock(ctx); err != nil {
return fmt.Errorf("failed to get lock: %w", err)
}
logger.Logf(false, "get lock successfully!")
break
}
time.Sleep(time.Second)
}
eg.Go(func() error {
if err := web.Serve(ctx, m.ds); err != nil {
logger.Logf(false, "failed to web.Serve: %+v", err)
return fmt.Errorf("failed to serve: %w", err)
}
return nil
})
eg.Go(func() error {
if err := m.start.Loop(ctx); err != nil {
logger.Logf(false, "failed to starter manager: %+v", err)
return fmt.Errorf("failed to starter loop: %w", err)
}
return nil
})
eg.Go(func() error {
if err := m.run.Loop(ctx); err != nil {
logger.Logf(false, "failed to runner manager: %+v", err)
return fmt.Errorf("failed to runner loop: %w", err)
}
return nil
})
if err := eg.Wait(); err != nil {
return fmt.Errorf("failed to wait errgroup: %w", err)
}
return nil
}
================================================
FILE: cmd/shoes-tester/main.go
================================================
package main
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/hashicorp/go-plugin"
"github.com/whywaita/myshoes/pkg/config"
"github.com/whywaita/myshoes/pkg/datastore"
"github.com/whywaita/myshoes/pkg/gh"
"github.com/whywaita/myshoes/pkg/shoes"
"github.com/whywaita/myshoes/pkg/starter"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
subcommand := os.Args[1]
switch subcommand {
case "add":
if err := runAdd(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "delete":
if err := runDelete(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "Unknown subcommand: %s\n", subcommand)
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Fprintf(os.Stderr, `Usage: shoes-tester <command> [options]
Commands:
add Add an instance
delete Delete an instance
Run 'shoes-tester <command> --help' for more information on a command.
`)
}
type addFlags struct {
pluginPath string
runnerName string
resourceType string
labels string
setupScript string
generateScript bool
scope string
githubAppID string
githubPrivateKeyPath string
runnerVersion string
runnerUser string
runnerBaseDirectory string
githubURL string
jsonOutput bool
}
type deleteFlags struct {
pluginPath string
cloudID string
labels string
jsonOutput bool
}
func runAdd(args []string) error {
fs := flag.NewFlagSet("add", flag.ExitOnError)
flags := &addFlags{}
fs.StringVar(&flags.pluginPath, "plugin", "", "Path to shoes-provider binary (required)")
fs.StringVar(&flags.runnerName, "runner-name", "", "Runner name (required)")
fs.StringVar(&flags.resourceType, "resource-type", "nano", "Resource type (nano|micro|small|medium|large|xlarge|2xlarge|3xlarge|4xlarge)")
fs.StringVar(&flags.labels, "labels", "", "Comma-separated labels")
fs.StringVar(&flags.setupScript, "setup-script", "", "Setup script (simple mode)")
fs.BoolVar(&flags.generateScript, "generate-script", false, "Generate setup script (script generation mode)")
fs.StringVar(&flags.scope, "scope", "", "Repository (owner/repo) or Organization (script generation mode)")
fs.StringVar(&flags.githubAppID, "github-app-id", os.Getenv("GITHUB_APP_ID"), "GitHub App ID (script generation mode)")
fs.StringVar(&flags.githubPrivateKeyPath, "github-private-key-path", os.Getenv("GITHUB_PRIVATE_KEY_PATH"), "GitHub App private key path (script generation mode)")
fs.StringVar(&flags.runnerVersion, "runner-version", "latest", "Runner version (script generation mode)")
fs.StringVar(&flags.runnerUser, "runner-user", "runner", "Runner user (script generation mode)")
fs.StringVar(&flags.runnerBaseDirectory, "runner-base-directory", "/tmp", "Runner base directory (script generation mode)")
fs.StringVar(&flags.githubURL, "github-url", "", "GitHub Enterprise Server URL (script generation mode)")
fs.BoolVar(&flags.jsonOutput, "json", false, "Output in JSON format")
fs.Parse(args)
if flags.pluginPath == "" {
return fmt.Errorf("--plugin is required")
}
if flags.runnerName == "" {
return fmt.Errorf("--runner-name is required")
}
if flags.generateScript {
if flags.scope == "" {
return fmt.Errorf("--scope is required for script generation mode")
}
if flags.githubAppID == "" {
return fmt.Errorf("--github-app-id is required for script generation mode")
}
if flags.githubPrivateKeyPath == "" {
return fmt.Errorf("--github-private-key-path is required for script generation mode")
}
}
ctx := context.Background()
var setupScript string
if flags.generateScript {
script, err := generateSetupScript(ctx, flags)
if err != nil {
return fmt.Errorf("failed to generate setup script: %w", err)
}
setupScript = script
} else {
setupScript = flags.setupScript
}
resourceType := datastore.UnmarshalResourceTypeString(flags.resourceType)
if resourceType == datastore.ResourceTypeUnknown && flags.resourceType != "unknown" {
return fmt.Errorf("invalid resource type: %s", flags.resourceType)
}
labels := parseLabels(flags.labels)
client, teardown, err := getClientWithPath(flags.pluginPath)
if err != nil {
return fmt.Errorf("failed to get plugin client: %w", err)
}
defer teardown()
cloudID, ipAddress, shoesType, actualResourceType, err := client.AddInstance(ctx, flags.runnerName, setupScript, resourceType, labels)
if err != nil {
return fmt.Errorf("failed to add instance: %w", err)
}
if flags.jsonOutput {
output := map[string]string{
"cloud_id": cloudID,
"ip_address": ipAddress,
"shoes_type": shoesType,
"resource_type": actualResourceType.String(),
}
data, err := json.Marshal(output)
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
fmt.Println(string(data))
} else {
fmt.Printf("AddInstance succeeded:\n")
fmt.Printf(" Cloud ID: %s\n", cloudID)
fmt.Printf(" Shoes Type: %s\n", shoesType)
fmt.Printf(" IP Address: %s\n", ipAddress)
fmt.Printf(" Resource Type: %s\n", actualResourceType.String())
}
return nil
}
func runDelete(args []string) error {
fs := flag.NewFlagSet("delete", flag.ExitOnError)
flags := &deleteFlags{}
fs.StringVar(&flags.pluginPath, "plugin", "", "Path to shoes-provider binary (required)")
fs.StringVar(&flags.cloudID, "cloud-id", "", "Cloud ID (required)")
fs.StringVar(&flags.labels, "labels", "", "Comma-separated labels")
fs.BoolVar(&flags.jsonOutput, "json", false, "Output in JSON format")
fs.Parse(args)
if flags.pluginPath == "" {
return fmt.Errorf("--plugin is required")
}
if flags.cloudID == "" {
return fmt.Errorf("--cloud-id is required")
}
ctx := context.Background()
labels := parseLabels(flags.labels)
client, teardown, err := getClientWithPath(flags.pluginPath)
if err != nil {
return fmt.Errorf("failed to get plugin client: %w", err)
}
defer teardown()
if err := client.DeleteInstance(ctx, flags.cloudID, labels); err != nil {
return fmt.Errorf("failed to delete instance: %w", err)
}
if flags.jsonOutput {
output := map[string]string{
"status": "success",
}
data, err := json.Marshal(output)
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
fmt.Println(string(data))
} else {
fmt.Printf("DeleteInstance succeeded\n")
}
return nil
}
func getClientWithPath(pluginPath string) (shoes.Client, func(), error) {
handshake := plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "SHOES_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "are_you_a_shoes?",
}
pluginMap := map[string]plugin.Plugin{
"shoes_grpc": &shoes.Plugin{},
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshake,
Plugins: pluginMap,
Cmd: exec.Command(pluginPath),
Managed: true,
Stderr: os.Stderr,
SyncStdout: os.Stdout,
SyncStderr: os.Stderr,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
})
rpcClient, err := client.Client()
if err != nil {
return nil, nil, fmt.Errorf("failed to get shoes client: %w", err)
}
raw, err := rpcClient.Dispense("shoes_grpc")
if err != nil {
return nil, nil, fmt.Errorf("failed to shoes client instance: %w", err)
}
return raw.(shoes.Client), client.Kill, nil
}
func generateSetupScript(ctx context.Context, flags *addFlags) (string, error) {
keyBytes, err := os.ReadFile(flags.githubPrivateKeyPath)
if err != nil {
return "", fmt.Errorf("failed to read private key: %w", err)
}
appID, err := strconv.ParseInt(flags.githubAppID, 10, 64)
if err != nil {
return "", fmt.Errorf("failed to parse github-app-id: %w", err)
}
block, _ := pem.Decode(keyBytes)
if block == nil {
return "", fmt.Errorf("failed to decode PEM block from private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse private key: %w", err)
}
config.Config.GitHub.AppID = appID
config.Config.GitHub.PEMByte = keyBytes
config.Config.GitHub.PEM = privateKey
if flags.githubURL == "" {
config.Config.GitHubURL = "https://github.com"
} else {
config.Config.GitHubURL = flags.githubURL
}
config.Config.RunnerUser = flags.runnerUser
config.Config.RunnerBaseDirectory = flags.runnerBaseDirectory
if err := gh.InitializeCache(appID, keyBytes); err != nil {
return "", fmt.Errorf("failed to initialize GitHub client cache: %w", err)
}
s := starter.New(nil, nil, flags.runnerVersion, nil)
return s.GetSetupScript(ctx, flags.scope, flags.runnerName)
}
func parseLabels(labels string) []string {
if labels == "" {
return []string{}
}
parts := strings.Split(labels, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
================================================
FILE: docs/01_01_for_admin_setup.md
================================================
# Setup myshoes daemon
## Goal
- Start myshoes daemon
## Prepare
- The network connectivity to myshoes server.
- The webhook endpoint from github.com **OR** your GitHub Enterprise Server (`POST /github/events`).
- REST API from your workspace (`GET, POST, DELETE /target`).
- You decide platform for runner and shoes-provider
- The official shoes-provider topic is [myshoes-provider](https://github.com/search?q=topic%3Amyshoes-provider).
- You can implement and use your private shoes-provider. Please check [how-to-develop-shoes.md](./03_how-to-develop-shoes.md).
## Word definition
- `your_shoes_host`: The endpoint of serving myshoes.
- e.g.) `https://myshoes.example.com`
## Setup
Please prepare a few things first.
### Machine image for runner
- Virtual Machine Image on your cloud provider.
- installed a some commands.
- required: curl (1)
- optional: jq (1), docker (1)
- optional, but **STRONG RECOMMEND INSTALLING BEFORE** (please read known issue)
- put latest runner tar.gz to `/usr/local/etc` [optional]
- optional, but **STRONG RECOMMEND INSTALLING BEFORE** (please read known issue)
For example is [here](https://github.com/whywaita/myshoes-providers/tree/master/shoes-lxd/images). (packer file)
### Create GitHub Apps
#### Configure values
- GitHub App Name: any text
- Homepage URL: any text
##### Webhook
- Webhook URL: `${your_shoes_host}/github/events`
- Webhook secret: any text
##### Repository permissions
- Actions: Read-only
- Administration: Read & write
- Checks: Read-only
##### Organization permissions
- Self-hosted runners: Read & write
##### Subscribe to events
- Check `Workflow job`
### Download private key
- download from GitHub or upload private key from your machine.
### Running
```bash
$ make build
$ ./myshoes
```
A config variables can set from environment values.
- `PORT`
- default: 8080
- Listen port for myshoes.
- GitHub Apps information
- required
- `GITHUB_APP_ID`
- `GITHUB_APP_SECRET` (if you set `Webhook secret` for your GitHub App)
- `GITHUB_PRIVATE_KEY_BASE64`
- base64 encoded private key from GitHub Apps
- `$ cat privatekey.pem | base64 -w 0`
- `MYSQL_URL`
- required
- DataSource Name, ex) `username:password@tcp(localhost:3306)/myshoes`
- if `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE` all are set, this env value are ignored.
- `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE`
- optional
- If all environment variables are set, mysql_url is constructed and loaded in the following way, then `MYSQL_URL` env will be ignored.
- example) `${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DATABASE}`
- `PLUGIN`
- 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.
- Please contain schema.
- `RUNNER_VERSION`
- default: `latest`
- Use the latest version in starting job
- The version of `actions/runner`
- example) `v2.302.1`, `latest`
- `RUNNER_USER`
- default: `runner`
- set linux username that executes runner. you need to set exist user.
- DO NOT set root. It can't run GitHub Actions runner in root permission.
- Example: `ubuntu`
- `PROVIDE_DOCKER_HUB_METRICS`
- default: `false`
- set `true` if you want to provide rate-limit metrics for Docker Hub.
- If you're not anonymous user, you need to set `DOCKER_HUB_USERNAME` and `DOCKER_HUB_PASSWORD`.
- `DOCKER_HUB_USERNAME`
- default: `` (empty)
- set Docker Hub username for pulling Docker image. (Use for provide rate-limit metrics)
- `DOCKER_HUB_PASSWORD`
- default: `` (empty)
- set Docker Hub password for pulling Docker image. (Use for provide rate-limit metrics)
For tuning values
- `DEBUG`
- default: false
- show debugging log
- `STRICT`
- default: true
- set strict mode
- `MODE_WEBHOOK_TYPE`
- default: `workflow_job` (use receive `workflow_job` event)
- Set type of webhook from GitHub
- option: `check_run`
- `MAX_CONNECTIONS_TO_BACKEND`
- default: 50
- The number of max connections to shoes-provider
- `MAX_CONCURRENCY_DELETING`
- default: 1
- The number of max concurrency of deleting
and more some env values from [shoes provider](https://github.com/search?q=topic%3Amyshoes-provider).
================================================
FILE: docs/01_02_for_admin_tips.md
================================================
# Tips for myshoes admin
## Job management hooks for self-hosted runners
You can use job management hooks for self-hosted runners.
Please set script file to your runner image.
- `ACTIONS_RUNNER_HOOK_JOB_STARTED`: `/myshoes-actions-runner-hook-job-started.sh`
- `ACTIONS_RUNNER_HOOK_JOB_COMPLETED`: `/myshoes-actions-runner-hook-job-completed.sh`
================================================
FILE: docs/02_01_for_user_setup.md
================================================
# Setup (only once)
## Goal
- Start provision runner
## Prepare
- Get GitHub Apps's Public page from myshoes admin.
- e.g.) `https://github.com/apps/<GitHub Apps name>` or `<GHES url>/github-apps/<GitHub Apps name>`
- Get Endpoint for myshoes from myshoes admin.
- e.g.) `<your_shoes_host>/target`
## Repository or Organization setup
### Install GitHub Apps
Please open GitHub Apps's Public page and install GitHub Apps to Organization or repository.


### Register target to myshoes
you need to register a target that repository or organization.
- `scope`: set target scope for an auto-scaling runner.
- Repository example: `octocat/hello-worlds`
- Organization example: `octocat`
- `resource_type`: set instance size for a runner.
- We will describe later.
- Please teach it from myshoes admin.
Example (create a target):
```bash
$ curl -XPOST -d '{"scope": "octocat/hello-world", "resource_type": "micro"}' ${your_shoes_host}/target
```
You can check registered targets.
```bash
curl -XGET ${your_shoes_host}/target | jq .
[
{
"id": "477f6073-90d1-47d8-958f-4707cea61e8d",
"scope": "octocat",
"token_expired_at": "2006-01-02T15:04:05Z",
"resource_type": "micro",
"provider_url": "",
"status": "active",
"status_description": "",
"created_at": "2006-01-02T15:04:05Z",
"updated_at": "2006-01-02T15:04:05Z"
}
]
```
#### Switch `resource_type`
You can set `resource_type` in target. So myshoes switch size of instance.
For example,
- In organization scope (`octocat`), want to set small size runner as a `nano`.
- But specific repository (`octocat/huge-repository`), want to set big size runner as a `4xlarge`.
So please configure it.
```bash
$ curl -XPOST -d '{"scope": "octocat", "resource_type": "nano"}' ${your_shoes_host}/target
$ curl -XPOST -d '{"scope": "octocat/huge-repository", "resource_type": "4xlarge"}' ${your_shoes_host}/target
$ curl -XGET ${your_shoes_host}/target | jq .
[
{
"id": "477f6073-90d1-47d8-958f-4707cea61e8d",
"scope": "octocat",
"token_expired_at": "2006-01-02T15:04:05Z",
"resource_type": "nano",
"provider_url": "",
"status": "active",
"status_description": "",
"created_at": "2006-01-02T15:04:05Z",
"updated_at": "2006-01-02T15:04:05Z"
},
{
"id": "3775e3b6-08e0-4abc-830d-fd5325397de0",
"scope": "octocat/huge-repository",
"token_expired_at": "2006-01-02T15:04:05Z",
"resource_type": "4xlarge",
"provider_url": "",
"status": "active",
"status_description": "",
"created_at": "2006-01-02T15:04:05Z",
"updated_at": "2006-01-02T15:04:05Z"
}
]
```
In this configuration, myshoes will create under it.
- In `octocat/normal-repository`, will create `nano`
- In `octocat/normal-repository2`, will create `nano`
- In `octocat/huge-repository`, will create `4xlarge`
### Create an offline runner (only use `check_run` mode)
GitHub Actions need offline runner if queueing job.
Please create an offline runner in the target repository.
https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners/adding-self-hosted-runners
Please delete a runner after registered.
After that, You can use [cycloud-io/refresh-runner-action](https://github.com/cycloud-io/refresh-runner-action) for automation.
### Let's go using your shoes!
Let's execute your jobs! :runner::runner::runner:
================================================
FILE: docs/03_how-to-develop-shoes.md
================================================
# How to develop shoes provider
## TL;DR
- implement to gRPC server
- `shoes`, `health`, `stdio`
- define resource type in your shoes provider's flavor.
## gRPC server
shoes provider use [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin).
you need to register three Service.
if you use a golang in development, you can use `pkg/pluginutils/setup.go`.
please check `plugins/shoes-mock`. There are mock shoes provider.
### shoes server
`shoes` is gRPC server. you need to implement two funtion.
- `AddInstance`
- `DeleteInstance`
please check `api/proto/myshoes.proto`.
### health
`health` is [grpc-ecosystem/grpc-health-probe](https://github.com/grpc-ecosystem/grpc-health-probe).
### stdio
`stdio` is standard I/O service.
this service communicate plugin binary's standard I/O.
## Resource type
myshoes defined some machine type. you need to map machine spec for your resource type.
- nano
- micro
- small
- medium
- large
- xlarge
- 2xlarge
- 3xlarge
- 4xlarge
## Testing shoes provider
`shoes-tester` is a CLI tool for testing shoes provider without running myshoes server.
### Build
```bash
go build -o shoes-tester ./cmd/shoes-tester
```
### Usage
#### Add instance
Simple mode (with setup script):
```bash
./shoes-tester add \
--plugin ./path/to/your-shoes-provider \
--runner-name test-runner \
--resource-type nano \
--labels "label1,label2" \
--setup-script "#!/bin/bash\necho 'setup'"
```
Script generation mode (generate setup script automatically):
```bash
./shoes-tester add \
--plugin ./path/to/your-shoes-provider \
--runner-name test-runner \
--resource-type nano \
--generate-script \
--scope owner/repo \
--github-app-id 123456 \
--github-private-key-path /path/to/key.pem \
--runner-version latest
```
#### Delete instance
```bash
./shoes-tester delete \
--plugin ./path/to/your-shoes-provider \
--cloud-id your-cloud-id \
--labels "label1,label2"
```
#### Options
Add command:
- `--plugin`: Path to shoes-provider binary (required)
- `--runner-name`: Runner name (required)
- `--resource-type`: Resource type (default: nano)
- `--labels`: Comma-separated labels
- `--setup-script`: Setup script (simple mode)
- `--generate-script`: Generate setup script automatically (script generation mode)
- `--scope`: Repository (owner/repo) or Organization (script generation mode)
- `--github-app-id`: GitHub App ID (script generation mode)
- `--github-private-key-path`: GitHub App private key path (script generation mode)
- `--runner-version`: Runner version (default: latest, script generation mode)
- `--runner-user`: Runner user (default: runner, script generation mode)
- `--runner-base-directory`: Runner base directory (default: /tmp, script generation mode)
- `--github-url`: GitHub Enterprise Server URL (script generation mode)
- `--json`: Output in JSON format
Delete command:
- `--plugin`: Path to shoes-provider binary (required)
- `--cloud-id`: Cloud ID (required)
- `--labels`: Comma-separated labels
- `--json`: Output in JSON format
================================================
FILE: docs/assets/myshoes.service
================================================
[Unit]
Description=myshoes is Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions
After=network.target
[Service]
User=root
EnvironmentFile=/etc/default/myshoes
ExecStart=/usr/local/bin/myshoes
Restart=always
[Install]
WantedBy=multi-user.target
================================================
FILE: go.mod
================================================
module github.com/whywaita/myshoes
go 1.25
require (
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/go-cmp v0.7.0
github.com/google/go-github/v80 v80.0.0
github.com/hashicorp/go-plugin v1.7.0
github.com/hashicorp/go-version v1.8.0
github.com/jmoiron/sqlx v1.4.0
github.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f
github.com/ory/dockertest/v3 v3.12.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.23.2
github.com/r3labs/diff/v2 v2.15.1
github.com/satori/go.uuid v1.2.0
goji.io v2.0.2+incompatible
golang.org/x/oauth2 v0.32.0
golang.org/x/sync v0.18.0
google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
)
require (
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.1.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-github/v75 v75.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.2.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.2.3 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8=
github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
github.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs=
github.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f h1:MBcrTbmCf7CZa9yAwcB7ArveQb9TPVy4zFnQGz/LiUU=
github.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f/go.mod h1:UawoqorwkpZ58qWiL+nVJM0Po7FrzAdCxYVh9GgTTaA=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg=
github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
================================================
FILE: internal/testutils/mysql.go
================================================
package testutils
import (
"fmt"
"log"
"os"
"path"
"runtime"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/whywaita/myshoes/pkg/datastore"
)
const schemaDirRelativePathFormat = "%s/../../pkg/datastore/mysql/%s"
func execSchema(fpath string) {
b, err := os.ReadFile(fpath)
if err != nil {
log.Fatalf("schema reading error: %v", err)
}
queries := strings.Split(string(b), ";")
for _, query := range queries[:len(queries)-1] {
_, err = testDB.Exec(query)
if err != nil {
log.Fatalf("exec schema error: %v, query: %s", err, query)
}
}
}
func createTablesIfNotExist() {
_, pwd, _, _ := runtime.Caller(0)
schemaPath := fmt.Sprintf(schemaDirRelativePathFormat, path.Dir(pwd), "schema.sql")
execSchema(schemaPath)
}
func truncateTables() {
rows, err := testDB.Query("SHOW TABLES")
if err != nil {
log.Fatalf("show tables error: %#v", err)
}
defer rows.Close()
for rows.Next() {
var tableName string
err = rows.Scan(&tableName)
if err != nil {
log.Fatalf("show table error: %#v", err)
continue
}
cmds := []string{
"SET FOREIGN_KEY_CHECKS = 0",
fmt.Sprintf("TRUNCATE %s", tableName),
"SET FOREIGN_KEY_CHECKS = 1",
}
for _, cmd := range cmds {
_, err := testDB.Exec(cmd)
if err != nil {
mysqlErr, ok := err.(*mysql.MySQLError)
if ok {
if mysqlErr.Number == 0xde2 {
// is rejected
continue
}
} else {
log.Fatalf("truncate error: %#v", err)
continue
}
}
}
}
}
// GetTestDatastore return pointer of datastore
func GetTestDatastore() (datastore.Datastore, func()) {
if testDatastore == nil {
panic("datastore is not initialized yet")
}
return testDatastore, func() { truncateTables() }
}
// GetTestDB return pointer of testDB
func GetTestDB() (*sqlx.DB, func()) {
if testDB == nil {
panic("testDB is not initialized yet")
}
return testDB, func() { truncateTables() }
}
================================================
FILE: internal/testutils/testutils.go
================================================
package testutils
import (
"fmt"
"log"
"net/http/httptest"
"testing"
"github.com/whywaita/myshoes/pkg/datastore"
"github.com/whywaita/myshoes/pkg/datastore/mysql"
"github.com/whywaita/myshoes/pkg/web"
"github.com/jmoiron/sqlx"
"github.com/ory/dockertest/v3"
)
const (
mysqlRootPassword = "secret"
)
var (
testDB *sqlx.DB
testDatastore datastore.Datastore
testURL string
)
// IntegrationTestRunner is all integration test
func IntegrationTestRunner(m *testing.M) int {
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
// pulls an image, creates a container based on it and runs it
resource, err := pool.Run("mysql", "8.0", []string{"MYSQL_ROOT_PASSWORD=" + mysqlRootPassword})
if err != nil {
log.Fatalf("Could not start resource: %s", err)
}
// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
if err := pool.Retry(func() error {
var err error
dsn := fmt.Sprintf("root:%s@(localhost:%s)/mysql", mysqlRootPassword, resource.GetPort("3306/tcp"))
testDatastore, err = mysql.New(dsn, make(chan<- struct{}))
if err != nil {
log.Fatalf("failed to create datastore instance: %s", err)
}
testDB, err = sqlx.Open("mysql", fmt.Sprintf("root:%s@(localhost:%s)/mysql?parseTime=true&loc=UTC", mysqlRootPassword, resource.GetPort("3306/tcp")))
if err != nil {
return err
}
return testDB.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
createTablesIfNotExist()
//SetupDefaultFixtures()
mux := web.NewMux(testDatastore)
ts := httptest.NewServer(mux)
testURL = ts.URL
code := m.Run()
ts.Close()
truncateTables()
// You can't defer this because os.Exit doesn't care for defer
if err := pool.Purge(resource); err != nil {
log.Fatalf("Could not purge resource: %s", err)
}
return code
}
================================================
FILE: internal/testutils/web.go
================================================
package testutils
// GetTestURL return url of httptest.Server
func GetTestURL() string {
if testURL == "" {
panic("testURL is not initialized yet")
}
return testURL
}
================================================
FILE: internal/util/util.go
================================================
package util
import (
"math/rand/v2"
"time"
)
// CalcRetryTime is caliculate retry time by exponential backoff and jitter
func CalcRetryTime(count int) time.Duration {
if count == 0 {
return 0
}
backoff := 1 << count
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
return time.Duration(backoff)*time.Second + jitter
}
================================================
FILE: pkg/config/config.go
================================================
package config
import (
"crypto/rsa"
"strings"
)
// Config is config value
var Config Conf
// Conf is type of Config
type Conf struct {
GitHub GitHubApp
MySQLDSN string
Port int
ShoesPluginPath string
ShoesPluginOutputPath string
RunnerUser string
RunnerBaseDirectory string
Debug bool
Strict bool // check to registered runner before delete job
ModeWebhookType ModeWebhookType
MaxConnectionsToBackend int64
MaxConcurrencyDeleting int64
GitHubURL string
RunnerVersion string
DockerHubCredential DockerHubCredential
ProvideDockerHubMetrics bool
}
// DockerHubCredential is type of config value
type DockerHubCredential struct {
Username string
Password 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"
EnvGitHubAppSecret = "GITHUB_APP_SECRET"
EnvGitHubAppPrivateKeyBase64 = "GITHUB_PRIVATE_KEY_BASE64"
EnvMySQLHost = "MYSQL_HOST"
EnvMySQLPort = "MYSQL_PORT"
EnvMySQLUser = "MYSQL_USER"
EnvMySQLPassword = "MYSQL_PASSWORD"
EnvMySQLDatabase = "MYSQL_DATABASE"
EnvMySQLURL = "MYSQL_URL"
EnvPort = "PORT"
EnvShoesPluginPath = "PLUGIN"
EnvShoesPluginOutputPath = "PLUGIN_OUTPUT"
EnvRunnerUser = "RUNNER_USER"
EnvRunnerBaseDirectory = "RUNNER_BASE_DIRECTORY"
EnvDebug = "DEBUG"
EnvStrict = "STRICT"
EnvModeWebhookType = "MODE_WEBHOOK_TYPE"
EnvMaxConnectionsToBackend = "MAX_CONNECTIONS_TO_BACKEND"
EnvMaxConcurrencyDeleting = "MAX_CONCURRENCY_DELETING"
EnvGitHubURL = "GITHUB_URL"
EnvRunnerVersion = "RUNNER_VERSION"
EnvDockerHubUsername = "DOCKER_HUB_USERNAME"
EnvDockerHubPassword = "DOCKER_HUB_PASSWORD"
EnvProvideDockerHubMetrics = "PROVIDE_DOCKER_HUB_METRICS"
)
// ModeWebhookType is type value for GitHub webhook
type ModeWebhookType int
const (
// ModeWebhookTypeUnknown is unknown
ModeWebhookTypeUnknown ModeWebhookType = iota
// ModeWebhookTypeCheckRun is check_run
ModeWebhookTypeCheckRun
// ModeWebhookTypeWorkflowJob is workflow_job
ModeWebhookTypeWorkflowJob
)
// String is implementation of fmt.Stringer
func (mwt ModeWebhookType) String() string {
unknown := "unknown"
switch mwt {
case ModeWebhookTypeUnknown:
return unknown
case ModeWebhookTypeCheckRun:
return "check_run"
case ModeWebhookTypeWorkflowJob:
return "workflow_job"
}
return unknown
}
// Equal check in and value
func (mwt ModeWebhookType) Equal(in string) bool {
return strings.EqualFold(in, mwt.String())
}
func marshalModeWebhookType(in string) ModeWebhookType {
switch in {
case "check_run":
return ModeWebhookTypeCheckRun
case "workflow_job":
return ModeWebhookTypeWorkflowJob
}
return ModeWebhookTypeUnknown
}
// IsGHES return myshoes for GitHub Enterprise Server
func (c Conf) IsGHES() bool {
return !strings.EqualFold(c.GitHubURL, "https://github.com")
}
================================================
FILE: pkg/config/init.go
================================================
package config
import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hashicorp/go-version"
)
// Load load config from environment
func Load() {
c := LoadWithDefault()
ga := LoadGitHubApps()
c.GitHub = *ga
pluginPath := LoadPluginPath()
c.ShoesPluginPath = pluginPath
Config = c
}
// LoadWithDefault load only value that has default value
func LoadWithDefault() Conf {
var c Conf
p := "8080"
if os.Getenv(EnvPort) != "" {
p = os.Getenv(EnvPort)
}
pp, err := strconv.Atoi(p)
if err != nil {
log.Panicf("failed to parse PORT: %+v", err)
}
c.Port = pp
runnerUser := "runner"
if os.Getenv(EnvRunnerUser) != "" {
runnerUser = os.Getenv(EnvRunnerUser)
}
c.RunnerUser = runnerUser
c.RunnerBaseDirectory = "/tmp"
if os.Getenv(EnvRunnerBaseDirectory) != "" {
c.RunnerBaseDirectory = os.Getenv(EnvRunnerBaseDirectory)
log.Printf("use runner base directory is %s\n", c.RunnerBaseDirectory)
}
c.Debug = false
if os.Getenv(EnvDebug) == "true" {
c.Debug = true
}
c.Strict = true
if os.Getenv(EnvStrict) == "false" {
c.Strict = false
}
c.ModeWebhookType = ModeWebhookTypeWorkflowJob
if os.Getenv(EnvModeWebhookType) != "" {
mwt := marshalModeWebhookType(os.Getenv(EnvModeWebhookType))
if mwt == ModeWebhookTypeUnknown {
log.Panicf("%s is invalid webhook type", os.Getenv(EnvModeWebhookType))
}
if mwt == ModeWebhookTypeCheckRun {
log.Println("WARNING: check_run is deprecated mode and will delete it. Please use workflow_job")
}
c.ModeWebhookType = mwt
}
c.ProvideDockerHubMetrics = false
if os.Getenv(EnvProvideDockerHubMetrics) == "true" {
c.ProvideDockerHubMetrics = true
}
c.DockerHubCredential = DockerHubCredential{}
if c.ProvideDockerHubMetrics {
if os.Getenv(EnvDockerHubUsername) != "" && os.Getenv(EnvDockerHubPassword) != "" {
c.DockerHubCredential.Username = os.Getenv(EnvDockerHubUsername)
c.DockerHubCredential.Password = os.Getenv(EnvDockerHubPassword)
} else {
log.Println("WARNING: Providing Docker Hub metrics is enabled, but DOCKER_HUB_USERNAME and DOCKER_HUB_PASSWORD are not set. Providing Docker Hub metrics with anonymous user mode")
}
} else {
log.Println("Docker Hub metrics is disabled")
}
c.MaxConnectionsToBackend = 50
if os.Getenv(EnvMaxConnectionsToBackend) != "" {
numberPB, err := strconv.ParseInt(os.Getenv(EnvMaxConnectionsToBackend), 10, 64)
if err != nil {
log.Panicf("failed to convert int64 %s: %+v", EnvMaxConnectionsToBackend, err)
}
c.MaxConnectionsToBackend = numberPB
}
c.MaxConcurrencyDeleting = 1
if os.Getenv(EnvMaxConcurrencyDeleting) != "" {
numberCD, err := strconv.ParseInt(os.Getenv(EnvMaxConcurrencyDeleting), 10, 64)
if err != nil {
log.Panicf("failed to convert int64 %s: %+v", EnvMaxConcurrencyDeleting, err)
}
c.MaxConcurrencyDeleting = numberCD
}
c.GitHubURL = "https://github.com"
if os.Getenv(EnvGitHubURL) != "" {
u, err := url.Parse(os.Getenv(EnvGitHubURL))
if err != nil {
log.Panicf("failed to parse URL %s: %+v", os.Getenv(EnvGitHubURL), err)
}
if strings.EqualFold(u.Scheme, "") {
log.Panicf("%s must has scheme (value: %s)", EnvGitHubURL, os.Getenv(EnvGitHubURL))
}
if strings.EqualFold(u.Host, "") {
log.Panicf("%s must has host (value: %s)", EnvGitHubURL, os.Getenv(EnvGitHubURL))
}
c.GitHubURL = os.Getenv(EnvGitHubURL)
}
if os.Getenv(EnvRunnerVersion) == "" {
c.RunnerVersion = "latest"
} else {
// valid value: "latest" or "vX.XXX.X"
switch os.Getenv(EnvRunnerVersion) {
case "latest":
c.RunnerVersion = "latest"
default:
_, err := version.NewVersion(os.Getenv(EnvRunnerVersion))
if err != nil {
log.Panicf("failed to parse input runner version: %+v", err)
}
c.RunnerVersion = os.Getenv(EnvRunnerVersion)
}
}
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 {
mysqlHost, ok_Host := os.LookupEnv(EnvMySQLHost)
mysqlPort, ok_Port := os.LookupEnv(EnvMySQLPort)
mysqlUser, ok_User := os.LookupEnv(EnvMySQLUser)
mysqlPassword, ok_Password := os.LookupEnv(EnvMySQLPassword)
mysqlDatabase, ok_Database := os.LookupEnv(EnvMySQLDatabase)
if ok_Host && ok_Port && ok_User && ok_Password && ok_Database {
mysqlURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", mysqlUser, mysqlPassword, mysqlHost, mysqlPort, mysqlDatabase)
log.Println("load MySQL URL from environment variables MYSQL_USER, MYSQL_PASSWORD, MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, not MYSQL_URL")
return mysqlURL
}
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) {
f, err := os.ReadFile(p)
if err != nil {
return "", fmt.Errorf("failed to open file: %w", err)
}
// check binary type
mineType := http.DetectContentType(f)
if !strings.EqualFold(mineType, "application/octet-stream") {
return "", fmt.Errorf("invalid file type (correct: application/octet-stream got: %s)", mineType)
}
// need permission of execute
if err := os.Chmod(p, 0777); err != nil {
return "", fmt.Errorf("failed to chmod: %w", err)
}
if filepath.IsAbs(p) {
return p, nil
}
apath, err := filepath.Abs(p)
if err != nil {
return "", fmt.Errorf("failed to get abs: %w", err)
}
return apath, nil
}
// fetch retrieve plugin binaries.
// return saved file path.
func fetch(p string) (string, error) {
_, err := os.Stat(p)
if err == nil {
// this is file path!
return p, nil
}
u, err := url.Parse(p)
if err != nil {
return "", fmt.Errorf("failed to parse input url: %w", err)
}
switch u.Scheme {
case "http", "https":
return fetchHTTP(u)
default:
return "", fmt.Errorf("unsupported fetch schema (scheme: %s)", u.Scheme)
}
}
// fetchHTTP fetch plugin binary over HTTP(s).
// save to current directory.
func fetchHTTP(u *url.URL) (string, error) {
log.Printf("fetch plugin binary from %s\n", u.String())
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(dir, fileName)
f, err := os.Create(fp)
if err != nil {
return "", fmt.Errorf("failed to create os file: %w", err)
}
defer f.Close()
resp, err := http.Get(u.String())
if err != nil {
return "", fmt.Errorf("failed to get config via HTTP(S): %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get config via HTTP(S): status code is not 200 (status code: %d)", resp.StatusCode)
}
if _, err := io.Copy(f, resp.Body); err != nil {
return "", fmt.Errorf("failed to write file (path: %s): %w", fp, err)
}
return fp, nil
}
================================================
FILE: pkg/datastore/github.go
================================================
package datastore
import (
"context"
"fmt"
"net/url"
"sort"
"strings"
"sync"
"time"
"github.com/whywaita/myshoes/pkg/logger"
"github.com/google/go-github/v80/github"
"github.com/whywaita/myshoes/pkg/gh"
)
// NewClientInstallationByRepo create a client of GitHub using installation ID from repo name
func NewClientInstallationByRepo(ctx context.Context, ds Datastore, repo string) (*github.Client, *Target, error) {
target, err := SearchRepo(ctx, ds, repo)
if err != nil {
return nil, nil, fmt.Errorf("failed to search repository: %w", err)
}
installationID, err := gh.IsInstalledGitHubApp(ctx, target.Scope)
if err != nil {
return nil, nil, fmt.Errorf("failed to get installation ID: %w", err)
}
client, err := gh.NewClientInstallation(installationID)
if err != nil {
return nil, nil, fmt.Errorf("failed to create client: %w", err)
}
return client, target, nil
}
// PendingWorkflowRunWithTarget is struct for pending workflow run
type PendingWorkflowRunWithTarget struct {
Target *Target
WorkflowRun *github.WorkflowRun
}
// GetPendingWorkflowRunByRecentRepositories get pending workflow runs by recent active repositories
func GetPendingWorkflowRunByRecentRepositories(ctx context.Context, ds Datastore) ([]PendingWorkflowRunWithTarget, error) {
pendingRuns, err := getPendingWorkflowRunByRecentRepositories(ctx, ds)
if err != nil {
return nil, fmt.Errorf("failed to get pending workflow runs: %w", err)
}
queuedJob, err := ds.ListJobs(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get list of jobs: %w", err)
}
var result []PendingWorkflowRunWithTarget
// We ignore the pending run if the job is already queued.
for _, pendingRun := range pendingRuns {
found := false
for _, job := range queuedJob {
webhookEvent, err := github.ParseWebHook("workflow_job", []byte(job.CheckEventJSON))
if err != nil {
logger.Logf(false, "failed to parse webhook payload (job id: %s): %+v", job.UUID, err)
continue
}
workflowJob, ok := webhookEvent.(*github.WorkflowJobEvent)
if !ok {
logger.Logf(false, "failed to cast to WorkflowJobEvent (job id: %s)", job.UUID)
continue
}
if pendingRun.WorkflowRun.GetID() == workflowJob.GetWorkflowJob().GetRunID() {
logger.Logf(true, "found job in datastore, So will ignore: (repo: %s, gh_run_id: %d, gh_job_id: %d)", pendingRun.WorkflowRun.GetRepository().GetFullName(), pendingRun.WorkflowRun.GetID(), workflowJob.GetWorkflowJob().GetID())
found = true
break
}
}
if !found {
result = append(result, pendingRun)
}
}
return result, nil
}
func getPendingWorkflowRunByRecentRepositories(ctx context.Context, ds Datastore) ([]PendingWorkflowRunWithTarget, error) {
recentActiveRepositories, err := getRecentRepositories(ctx, ds)
if err != nil {
return nil, fmt.Errorf("failed to get recent repositories: %w", err)
}
var pendingRuns []PendingWorkflowRunWithTarget
var wg sync.WaitGroup
var mu sync.Mutex
for _, repoRawURL := range recentActiveRepositories {
wg.Add(1)
go func(repoRawURL string) {
defer wg.Done()
u, err := url.Parse(repoRawURL)
if err != nil {
logger.Logf(false, "failed to get pending run by recent repositories: failed to parse repository url: %+v", err)
return
}
fullName := strings.TrimPrefix(u.Path, "/")
client, target, err := NewClientInstallationByRepo(ctx, ds, fullName)
if err != nil {
logger.Logf(false, "failed to get pending run by recent repositories: failed to create a client of GitHub by repo (full_name: %s) %+v", fullName, err)
return
}
owner, repo := gh.DivideScope(fullName)
pendingRunsByRepo, err := getPendingRunByRepo(ctx, client, owner, repo)
if err != nil {
logger.Logf(false, "failed to get pending run by recent repositories: failed to get pending run by repo (full_name: %s) %+v", fullName, err)
return
}
mu.Lock()
for _, run := range pendingRunsByRepo {
pendingRuns = append(pendingRuns, PendingWorkflowRunWithTarget{
Target: target,
WorkflowRun: run,
})
}
mu.Unlock()
}(repoRawURL)
}
wg.Wait()
return pendingRuns, nil
}
func getPendingRunByRepo(ctx context.Context, client *github.Client, owner, repo string) ([]*github.WorkflowRun, error) {
runs, err := gh.ListWorkflowRunsNewest(ctx, client, owner, repo, 50)
if err != nil {
return nil, fmt.Errorf("failed to list runs: %w", err)
}
var pendingRuns []*github.WorkflowRun
for _, r := range runs {
if r.GetStatus() == "queued" || r.GetStatus() == "pending" {
oldMinutes := 10
sinceMinutes := time.Since(r.CreatedAt.Time).Minutes()
if sinceMinutes >= float64(oldMinutes) {
logger.Logf(false, "workflow run %d is pending over %d minutes, So will enqueue (repo: %s/%s)", r.GetID(), oldMinutes, owner, repo)
pendingRuns = append(pendingRuns, r)
} else {
logger.Logf(true, "workflow run %d is pending, but not over %d minutes. So ignore (since: %f minutes, repo: %s/%s)", r.GetID(), oldMinutes, sinceMinutes, owner, repo)
}
}
}
return pendingRuns, nil
}
func getRecentRepositories(ctx context.Context, ds Datastore) ([]string, error) {
recent := time.Now().Add(-1 * time.Hour)
recentRunners, err := ds.ListRunnersLogBySince(ctx, recent)
if err != nil {
return nil, fmt.Errorf("failed to get targets from datastore: %w", err)
}
// sort by created_at
sort.SliceStable(recentRunners, func(i, j int) bool {
return recentRunners[i].CreatedAt.After(recentRunners[j].CreatedAt)
})
// unique repositories
recentActiveRepositories := make(map[string]struct{})
for _, r := range recentRunners {
u := r.RepositoryURL
if _, ok := recentActiveRepositories[u]; !ok {
recentActiveRepositories[u] = struct{}{}
}
}
var result []string
for repository := range recentActiveRepositories {
result = append(result, repository)
}
return result, nil
}
================================================
FILE: pkg/datastore/interface.go
================================================
package datastore
import (
"context"
"database/sql"
"errors"
"fmt"
"net/url"
"strings"
"time"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/pkg/gh"
"github.com/whywaita/myshoes/pkg/logger"
)
// Error values
var (
ErrNotFound = errors.New("not found")
)
// Lock values
var (
IsLocked = "is locked"
IsNotLocked = "is not locked"
)
// Datastore is persistent storage
type Datastore interface {
CreateTarget(ctx context.Context, target Target) error
GetTarget(ctx context.Context, id uuid.UUID) (*Target, error)
GetTargetByScope(ctx context.Context, scope string) (*Target, error)
ListTargets(ctx context.Context) ([]Target, error)
DeleteTarget(ctx context.Context, id uuid.UUID) error
// Deprecated: Use datastore.UpdateTargetStatus.
UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus TargetStatus, description string) error
UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error
UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType ResourceType, newProviderURL sql.NullString) error
EnqueueJob(ctx context.Context, job Job) error
ListJobs(ctx context.Context) ([]Job, error)
DeleteJob(ctx context.Context, id uuid.UUID) error
CreateRunner(ctx context.Context, runner Runner) error
ListRunners(ctx context.Context) ([]Runner, error)
ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]Runner, error)
ListRunnersLogBySince(ctx context.Context, since time.Time) ([]Runner, error)
GetRunner(ctx context.Context, id uuid.UUID) (*Runner, error)
DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason RunnerStatus) error
// Lock
GetLock(ctx context.Context) error
IsLocked(ctx context.Context) (string, error)
}
// Target is a target repository that will add auto-scaling runner.
type Target struct {
UUID uuid.UUID `db:"uuid" json:"id"`
Scope string `db:"scope" json:"scope"` // repo (:owner/:repo) or org (:organization)
// deprecated
GitHubToken string `db:"github_token" json:"github_token"`
TokenExpiredAt time.Time `db:"token_expired_at" json:"token_expired_at"`
GHEDomain sql.NullString `db:"ghe_domain" json:"ghe_domain"`
ResourceType ResourceType `db:"resource_type" json:"resource_type"`
ProviderURL sql.NullString `db:"provider_url" json:"provider_url"`
Status TargetStatus `db:"status" json:"status"`
StatusDescription sql.NullString `db:"status_description" json:"status_description"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// OwnerRepo return :owner and :repo
func (t *Target) OwnerRepo() (string, string) {
return gh.DivideScope(t.Scope)
}
// CanReceiveJob check status in target
func (t *Target) CanReceiveJob() bool {
switch t.Status {
case TargetStatusSuspend, TargetStatusDeleted:
return false
}
return true
}
// ListTargets get list of target that can receive job
func ListTargets(ctx context.Context, ds Datastore) ([]Target, error) {
targets, err := ds.ListTargets(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get targets from datastore: %w", err)
}
var result []Target
for _, t := range targets {
if t.CanReceiveJob() {
result = append(result, t)
}
}
return result, nil
}
// UpdateTargetStatus update datastore
func UpdateTargetStatus(ctx context.Context, ds Datastore, targetID uuid.UUID, newStatus TargetStatus, description string) error {
target, err := ds.GetTarget(ctx, targetID)
if err != nil {
return fmt.Errorf("failed to get target: %w", err)
}
if !target.CanReceiveJob() {
// not change status
return nil
}
if err := ds.UpdateTargetStatus(ctx, targetID, newStatus, description); err != nil {
logger.Logf(false, "failed to update target status: %+v", err)
return err
}
return nil
}
// SearchRepo search datastore.Target from datastore
// format of repo is "orgs/repos"
func SearchRepo(ctx context.Context, ds Datastore, repo string) (*Target, error) {
sep := strings.Split(repo, "/")
if len(sep) != 2 {
return nil, fmt.Errorf("incorrect repo format ex: orgs/repo (input: %s)", repo)
}
// use repo scope if set repo
repoTarget, err := ds.GetTargetByScope(ctx, repo)
if err == nil && repoTarget.CanReceiveJob() {
return repoTarget, nil
} else if err != nil && !errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("failed to get target from repo: %w", err)
}
// repo is not found, so search org target
org := sep[0]
orgTarget, err := ds.GetTargetByScope(ctx, org)
if err != nil {
return nil, fmt.Errorf("failed to get target from organization: %w", err)
}
if !orgTarget.CanReceiveJob() {
return nil, fmt.Errorf("target is not active")
}
return orgTarget, nil
}
// TargetStatus is status for target
type TargetStatus string
// TargetStatus variables
const (
TargetStatusActive TargetStatus = "active" //lint:ignore SA9004 this is status
TargetStatusRunning = "running"
TargetStatusSuspend = "suspend"
TargetStatusDeleted = "deleted"
TargetStatusErr = "error"
)
// Job is a runner job
type Job struct {
UUID uuid.UUID `db:"uuid"`
GHEDomain sql.NullString `db:"ghe_domain"`
Repository string `db:"repository"` // repo (:owner/:repo)
CheckEventJSON string `db:"check_event"`
TargetID uuid.UUID `db:"target_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// RepoURL return repository URL that send webhook.
func (j *Job) RepoURL() string {
serverURL := "https://github.com"
if j.GHEDomain.Valid {
serverURL = j.GHEDomain.String
}
s := strings.Split(serverURL, "://")
var u url.URL
u.Scheme = s[0]
u.Host = s[1]
u.Path = j.Repository
return u.String()
}
// Runner is a runner
type Runner struct {
UUID uuid.UUID `db:"runner_id"`
ShoesType string `db:"shoes_type"`
IPAddress string `db:"ip_address"`
TargetID uuid.UUID `db:"target_id"`
CloudID string `db:"cloud_id"`
Deleted bool `db:"deleted"`
Status RunnerStatus `db:"status"`
ResourceType ResourceType `db:"resource_type"`
RunnerUser sql.NullString `db:"runner_user" json:"runner_user"`
ProviderURL sql.NullString `db:"provider_url" json:"provider_url"`
RepositoryURL string `db:"repository_url"`
RequestWebhook string `db:"request_webhook"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt sql.NullTime `db:"deleted_at"`
}
// RunnerStatus is status for runner
type RunnerStatus string
// RunnerStatus variables
const (
RunnerStatusCreated RunnerStatus = "created" //lint:ignore SA9004 this is status
RunnerStatusCompleted = "completed"
RunnerStatusReachHardLimit = "reach_hard_limit"
)
================================================
FILE: pkg/datastore/memory/memory.go
================================================
package memory
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/pkg/datastore"
)
// Memory is implement datastore on-memory
type Memory struct {
mu *sync.RWMutex
targets map[uuid.UUID]datastore.Target
jobs map[uuid.UUID]datastore.Job
runners map[uuid.UUID]datastore.Runner
}
// New create map
func New() (*Memory, error) {
m := &sync.RWMutex{}
t := map[uuid.UUID]datastore.Target{}
j := map[uuid.UUID]datastore.Job{}
r := map[uuid.UUID]datastore.Runner{}
return &Memory{
mu: m,
targets: t,
jobs: j,
runners: r,
}, nil
}
// CreateTarget create a target
func (m *Memory) CreateTarget(ctx context.Context, target datastore.Target) error {
m.mu.Lock()
defer m.mu.Unlock()
m.targets[target.UUID] = target
return nil
}
// GetTarget get a target
func (m *Memory) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) {
m.mu.RLock()
defer m.mu.RUnlock()
t, ok := m.targets[id]
if !ok {
return nil, datastore.ErrNotFound
}
return &t, nil
}
// GetTargetByScope get a target from scope
func (m *Memory) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, t := range m.targets {
if t.Scope == scope {
// found
return &t, nil
}
}
return nil, datastore.ErrNotFound
}
// ListTargets get a all targets
func (m *Memory) ListTargets(ctx context.Context) ([]datastore.Target, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var targets []datastore.Target
for _, t := range m.targets {
targets = append(targets, t)
}
return targets, nil
}
// DeleteTarget delete a target
func (m *Memory) DeleteTarget(ctx context.Context, id uuid.UUID) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.targets, id)
return nil
}
// UpdateTargetStatus update status in target
func (m *Memory) UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus datastore.TargetStatus, description string) error {
m.mu.Lock()
defer m.mu.Unlock()
t, ok := m.targets[targetID]
if !ok {
return fmt.Errorf("not found")
}
t.Status = newStatus
if description != "" {
t.StatusDescription.Valid = true
} else {
t.StatusDescription.Valid = false
}
t.StatusDescription.String = description
m.targets[targetID] = t
return nil
}
// UpdateToken update token in target
func (m *Memory) UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error {
m.mu.Lock()
defer m.mu.Unlock()
t, ok := m.targets[targetID]
if !ok {
return fmt.Errorf("not found")
}
t.GitHubToken = newToken
t.TokenExpiredAt = newExpiredAt
m.targets[targetID] = t
return nil
}
// UpdateTargetParam update parameter of target
func (m *Memory) UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType datastore.ResourceType, newProviderURL string) error {
m.mu.Lock()
defer m.mu.Unlock()
t, ok := m.targets[targetID]
if !ok {
return fmt.Errorf("not found")
}
t.ResourceType = newResourceType
t.ProviderURL = sql.NullString{
String: newProviderURL,
Valid: true,
}
m.targets[targetID] = t
return nil
}
// EnqueueJob add a job
func (m *Memory) EnqueueJob(ctx context.Context, job datastore.Job) error {
m.mu.Lock()
defer m.mu.Unlock()
m.jobs[job.UUID] = job
return nil
}
// ListJobs get all jobs
func (m *Memory) ListJobs(ctx context.Context) ([]datastore.Job, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var jobs []datastore.Job
for _, j := range m.jobs {
jobs = append(jobs, j)
}
return jobs, nil
}
// DeleteJob delete a job
func (m *Memory) DeleteJob(ctx context.Context, id uuid.UUID) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.jobs, id)
return nil
}
// CreateRunner add a runner
func (m *Memory) CreateRunner(ctx context.Context, runner datastore.Runner) error {
m.mu.Lock()
defer m.mu.Unlock()
m.runners[runner.UUID] = runner
return nil
}
// ListRunners get a all runners
func (m *Memory) ListRunners(ctx context.Context) ([]datastore.Runner, error) {
m.mu.Lock()
defer m.mu.Unlock()
var runners []datastore.Runner
for _, r := range m.runners {
runners = append(runners, r)
}
return runners, nil
}
// ListRunnersByTargetID get a not deleted runners that has target_id
func (m *Memory) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) {
m.mu.Lock()
defer m.mu.Unlock()
var runners []datastore.Runner
for _, r := range m.runners {
if uuid.Equal(r.TargetID, targetID) {
runners = append(runners, r)
}
}
return runners, nil
}
// ListRunnersLogBySince ListRunnerLog get a runners since time
func (m *Memory) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) {
m.mu.Lock()
defer m.mu.Unlock()
var runners []datastore.Runner
for _, r := range m.runners {
if r.CreatedAt.After(since) {
runners = append(runners, r)
}
}
return runners, nil
}
// GetRunner get a runner
func (m *Memory) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.runners[id]
if !ok {
return nil, datastore.ErrNotFound
}
return &r, nil
}
// DeleteRunner delete a runner
func (m *Memory) DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason datastore.RunnerStatus) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.runners, id)
return nil
}
// GetLock get lock
func (m *Memory) GetLock(ctx context.Context) error {
return nil
}
// IsLocked return status of lock
func (m *Memory) IsLocked(ctx context.Context) (string, error) {
return datastore.IsNotLocked, nil
}
================================================
FILE: pkg/datastore/mysql/job.go
================================================
package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/pkg/datastore"
)
// EnqueueJob add a job
func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error {
query := `INSERT INTO jobs(uuid, ghe_domain, repository, check_event, target_id) VALUES (?, ?, ?, ?, ?)`
if _, err := m.Conn.ExecContext(ctx, query, job.UUID, job.GHEDomain, job.Repository, job.CheckEventJSON, job.TargetID.String()); err != nil {
return fmt.Errorf("failed to execute INSERT query: %w", err)
}
select {
case m.notifyEnqueueCh <- struct{}{}:
// notified to starter
default:
// no capacity on channel, do not block
}
return nil
}
// ListJobs get all jobs
func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) {
var jobs []datastore.Job
query := `SELECT uuid, ghe_domain, repository, check_event, target_id, created_at, updated_at FROM jobs`
if err := m.Conn.SelectContext(ctx, &jobs, query); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return jobs, nil
}
// DeleteJob delete a job
func (m *MySQL) DeleteJob(ctx context.Context, id uuid.UUID) error {
query := `DELETE FROM jobs WHERE uuid = ?`
if _, err := m.Conn.ExecContext(ctx, query, id.String()); err != nil {
return fmt.Errorf("failed to execute DELETE query: %w", err)
}
return nil
}
================================================
FILE: pkg/datastore/mysql/job_test.go
================================================
package mysql_test
import (
"context"
"database/sql"
"errors"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/jmoiron/sqlx"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/internal/testutils"
"github.com/whywaita/myshoes/pkg/datastore"
)
var testJobID = uuid.FromStringOrNil("1b4e5b7a-e3c1-4829-9cfd-eac4183f2c95")
func TestMySQL_EnqueueJob(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GHEDomain: sql.NullString{
Valid: false,
},
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
tests := []struct {
input datastore.Job
want *datastore.Job
err bool
}{
{
input: datastore.Job{
UUID: testJobID,
Repository: testScopeRepo,
CheckEventJSON: `{"example": "json"}`,
TargetID: testTargetID,
},
want: &datastore.Job{
UUID: testJobID,
Repository: testScopeRepo,
CheckEventJSON: `{"example": "json"}`,
TargetID: testTargetID,
},
err: false,
},
}
for _, test := range tests {
err := testDatastore.EnqueueJob(context.Background(), test.input)
if !test.err && err != nil {
t.Fatalf("failed to enqueue job: %+v", err)
}
got, err := getJobFromSQL(testDB, test.input.UUID)
if err != nil {
t.Fatalf("failed to get job from SQL: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_ListJobs(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GHEDomain: sql.NullString{
Valid: false,
},
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
tests := []struct {
input []datastore.Job
want []datastore.Job
err bool
}{
{
input: []datastore.Job{
{
UUID: testJobID,
Repository: testScopeRepo,
CheckEventJSON: `{"example": "json"}`,
TargetID: testTargetID,
},
},
want: []datastore.Job{
{
UUID: testJobID,
Repository: testScopeRepo,
CheckEventJSON: `{"example": "json"}`,
TargetID: testTargetID,
},
},
err: false,
},
}
for _, test := range tests {
for _, input := range test.input {
err := testDatastore.EnqueueJob(context.Background(), input)
if !test.err && err != nil {
t.Fatalf("failed to enqueue job: %+v", err)
}
}
got, err := testDatastore.ListJobs(context.Background())
if err != nil {
t.Fatalf("failed to get jobs: %+v", err)
}
if len(test.want) != len(got) {
t.Fatalf("incorrect length jobs, want: %d but got: %d", len(test.want), len(got))
}
for i := range got {
got[i].CreatedAt = time.Time{}
got[i].UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_DeleteJob(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GHEDomain: sql.NullString{
Valid: false,
},
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
if err := testDatastore.EnqueueJob(context.Background(), datastore.Job{
UUID: testJobID,
Repository: testScopeRepo,
CheckEventJSON: `{"example": "json"}`,
TargetID: testTargetID,
}); err != nil {
t.Fatalf("failed to enqueue job: %+v", err)
}
tests := []struct {
input uuid.UUID
want *datastore.Job
err bool
}{
{
input: testJobID,
want: nil,
err: false,
},
}
for _, test := range tests {
err := testDatastore.DeleteJob(context.Background(), test.input)
if !test.err && err != nil {
t.Fatalf("failed to delete job: %+v", err)
}
got, err := getJobFromSQL(testDB, test.input)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("failed to get job from SQL: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func getJobFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Job, error) {
var j datastore.Job
query := `SELECT uuid, ghe_domain, repository, check_event, target_id FROM jobs WHERE uuid = ?`
stmt, err := testDB.Preparex(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare: %w", err)
}
err = stmt.Get(&j, id)
if err != nil {
return nil, fmt.Errorf("failed to get job: %w", err)
}
return &j, nil
}
================================================
FILE: pkg/datastore/mysql/lock.go
================================================
package mysql
import (
"context"
"fmt"
"github.com/go-sql-driver/mysql"
"github.com/whywaita/myshoes/pkg/config"
"github.com/whywaita/myshoes/pkg/datastore"
)
// GetLock get lock
func (m *MySQL) GetLock(ctx context.Context) error {
var res int
cfg, err := mysql.ParseDSN(config.Config.MySQLDSN)
if err != nil {
return fmt.Errorf("failed to parse DSN: %w", err)
}
lockKey := cfg.DBName
query := fmt.Sprintf(`SELECT GET_LOCK('%s', 10)`, lockKey)
if err := m.Conn.GetContext(ctx, &res, query); err != nil {
return fmt.Errorf("failed to GET_LOCK: %w", err)
}
return nil
}
// IsLocked return status of lock
func (m *MySQL) IsLocked(ctx context.Context) (string, error) {
var res int
cfg, err := mysql.ParseDSN(config.Config.MySQLDSN)
if err != nil {
return "", fmt.Errorf("failed to parse DSN: %w", err)
}
lockKey := cfg.DBName
query := fmt.Sprintf(`SELECT IS_FREE_LOCK('%s')`, lockKey)
if err := m.Conn.GetContext(ctx, &res, query); err != nil {
return "", fmt.Errorf("failed to IS_FREE_LOCK: %w", err)
}
switch res {
case 1:
return datastore.IsNotLocked, nil
case 0:
return datastore.IsLocked, nil
}
return "", fmt.Errorf("IS_FREE_LOCK return NULL")
}
================================================
FILE: pkg/datastore/mysql/mysql.go
================================================
package mysql
import (
"fmt"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
// MySQL is implement datastore in MySQL
type MySQL struct {
Conn *sqlx.DB
notifyEnqueueCh chan<- struct{}
}
// New create mysql connection
func New(dsn string, notifyEnqueueCh chan<- struct{}) (*MySQL, error) {
u, err := getMySQLURL(dsn)
if err != nil {
return nil, fmt.Errorf("failed to get MySQL URL: %w", err)
}
conn, err := sqlx.Open("mysql", u)
if err != nil {
return nil, fmt.Errorf("failed to create mysql connection: %w", err)
}
return &MySQL{
Conn: conn,
notifyEnqueueCh: notifyEnqueueCh,
}, nil
}
func getMySQLURL(dsn string) (string, error) {
c, err := mysql.ParseDSN(dsn)
if err != nil {
return "", fmt.Errorf("failed to parse DSN: %w", err)
}
c.Loc = time.UTC
c.ParseTime = true
c.Collation = "utf8mb4_general_ci"
if c.Params == nil {
c.Params = map[string]string{}
}
c.Params["sql_mode"] = "'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'"
c.InterpolateParams = true
return c.FormatDSN(), nil
}
================================================
FILE: pkg/datastore/mysql/mysql_test.go
================================================
package mysql_test
import (
"os"
"testing"
"github.com/whywaita/myshoes/internal/testutils"
)
func TestMain(m *testing.M) {
os.Exit(testutils.IntegrationTestRunner(m))
}
================================================
FILE: pkg/datastore/mysql/runner.go
================================================
package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/pkg/datastore"
)
// CreateRunner add a runner
func (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runner) error {
tx := m.Conn.MustBegin()
queryRunner := `INSERT INTO runners(uuid) VALUES (?)`
if _, err := tx.ExecContext(ctx, queryRunner, runner.UUID.String()); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute INSERT query runners: %w", err)
}
queryDetail := `INSERT INTO runner_detail(runner_id, shoes_type, ip_address, target_id, cloud_id, resource_type, runner_user, repository_url, request_webhook, provider_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
if _, err := tx.ExecContext(ctx, queryDetail, runner.UUID.String(), runner.ShoesType, runner.IPAddress, runner.TargetID.String(), runner.CloudID, runner.ResourceType, runner.RunnerUser, runner.RepositoryURL, runner.RequestWebhook, runner.ProviderURL); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute INSERT query runner_detail: %w", err)
}
queryRunning := `INSERT INTO runners_running(runner_id) VALUES (?)`
if _, err := tx.ExecContext(ctx, queryRunning, runner.UUID.String()); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute INSERT query runners_running: %w", err)
}
if err := tx.Commit(); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute COMMIT: %w", err)
}
return nil
}
// ListRunners get a not deleted runners
func (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, error) {
var runners []datastore.Runner
query := `SELECT runner.runner_id, detail.shoes_type, detail.ip_address, detail.target_id, detail.cloud_id, detail.created_at, detail.updated_at, detail.resource_type, detail.repository_url, detail.request_webhook, detail.runner_user, detail.provider_url
FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id`
err := m.Conn.SelectContext(ctx, &runners, query)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return runners, nil
}
// ListRunnersByTargetID get a not deleted runners that has target_id
func (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) {
var runners []datastore.Runner
query := `SELECT runner.runner_id, detail.shoes_type, detail.ip_address, detail.target_id, detail.cloud_id, detail.created_at, detail.updated_at, detail.resource_type, detail.repository_url, detail.request_webhook, detail.runner_user, detail.provider_url
FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id WHERE detail.target_id = ?`
err := m.Conn.SelectContext(ctx, &runners, query, targetID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return runners, nil
}
// ListRunnersLogBySince ListRunnerLog get a runners since time
func (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) {
var runners []datastore.Runner
query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE created_at > ?`
err := m.Conn.SelectContext(ctx, &runners, query, since)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return runners, nil
}
// GetRunner get a runner
func (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) {
var r datastore.Runner
query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE runner_id = ?`
if err := m.Conn.GetContext(ctx, &r, query, id.String()); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return &r, nil
}
// DeleteRunner delete a runner
func (m *MySQL) DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason datastore.RunnerStatus) error {
tx := m.Conn.MustBegin()
queryDelete := `DELETE FROM runners_running WHERE runner_id = ?`
if _, err := tx.ExecContext(ctx, queryDelete, id.String()); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute DELETE query: %w", err)
}
queryInsert := `INSERT INTO runners_deleted(runner_id, reason) VALUES (?, ?)`
if _, err := tx.ExecContext(ctx, queryInsert, id.String(), reason); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute INSERT query: %w", err)
}
if err := tx.Commit(); err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute COMMIT: %w", err)
}
return nil
}
================================================
FILE: pkg/datastore/mysql/runner_test.go
================================================
package mysql_test
import (
"context"
"database/sql"
"errors"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/jmoiron/sqlx"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/internal/testutils"
"github.com/whywaita/myshoes/pkg/datastore"
)
var testRunnerID = uuid.FromStringOrNil("7943e412-c0ae-4068-ab24-3e71a13fbe53")
func TestMySQL_CreateRunner(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
tests := []struct {
input datastore.Runner
want *datastore.Runner
err bool
}{
{
input: datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
want: &datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
err: false,
},
{
input: datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RunnerUser: sql.NullString{
String: "runner",
Valid: true,
},
ProviderURL: sql.NullString{
String: "./shoes-test",
Valid: true,
},
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
want: &datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RunnerUser: sql.NullString{
String: "runner",
Valid: true,
},
ProviderURL: sql.NullString{
String: "./shoes-test",
Valid: true,
},
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
err: false,
},
}
for _, test := range tests {
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
err := testDatastore.CreateRunner(context.Background(), test.input)
if !test.err && err != nil {
t.Fatalf("failed to create runner: %+v", err)
}
got, err := getRunnerFromSQL(testDB, test.input.UUID)
if err != nil {
t.Fatalf("failed to get runner from SQL: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
teardown()
}
}
func TestMySQL_ListRunners(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
tests := []struct {
input []datastore.Runner
want []datastore.Runner
err bool
}{
{
input: []datastore.Runner{
{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
},
want: []datastore.Runner{
{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
},
err: false,
},
}
for _, test := range tests {
for _, input := range test.input {
err := testDatastore.CreateRunner(context.Background(), input)
if !test.err && err != nil {
t.Fatalf("failed to create runner: %+v", err)
}
}
got, err := testDatastore.ListRunners(context.Background())
if err != nil {
t.Fatalf("failed to get runners: %+v", err)
}
if len(test.want) != len(got) {
t.Fatalf("incorrect length runners, want: %d but got: %d", len(test.want), len(got))
}
for i := range got {
got[i].CreatedAt = time.Time{}
got[i].UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
u := "00000000-0000-0000-0000-00000000000%d"
for i := 0; i < 3; i++ {
input := datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}
input.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
err := testDatastore.CreateRunner(context.Background(), input)
if err != nil {
t.Fatalf("failed to create runner: %+v", err)
}
}
err := testDatastore.DeleteRunner(context.Background(), uuid.FromStringOrNil(fmt.Sprintf(u, 0)), time.Now(), "deleted")
if err != nil {
t.Fatalf("failed to delete runner: %+v", err)
}
got, err := testDatastore.ListRunners(context.Background())
if err != nil {
t.Fatalf("failed to get runners: %+v", err)
}
for i := range got {
got[i].CreatedAt = time.Time{}
got[i].UpdatedAt = time.Time{}
}
var want []datastore.Runner
for i := 1; i < 3; i++ {
r := datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}
r.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
want = append(want, r)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
func TestMySQL_ListRunnersLogBySince(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
u := "00000000-0000-0000-0000-00000000000%d"
for i := 1; i < 3; i++ {
input := datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}
input.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
err := testDatastore.CreateRunner(context.Background(), input)
if err != nil {
t.Fatalf("failed to create runner: %+v", err)
}
time.Sleep(500 * time.Millisecond)
}
recent := time.Now().Add(-10 * time.Second)
got, err := testDatastore.ListRunnersLogBySince(context.Background(), recent)
if err != nil {
t.Fatalf("failed to get runners: %+v", err)
}
for i := range got {
got[i].CreatedAt = time.Time{}
got[i].UpdatedAt = time.Time{}
}
var want []datastore.Runner
for i := 1; i < 3; i++ {
r := datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}
r.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
want = append(want, r)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
func TestMySQL_GetRunner(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
if err := testDatastore.CreateRunner(context.Background(), datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}); err != nil {
t.Fatalf("failed to create runner: %+v", err)
}
tests := []struct {
input uuid.UUID
want *datastore.Runner
err bool
}{
{
input: testRunnerID,
want: &datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
},
err: false,
},
}
for _, test := range tests {
got, err := testDatastore.GetRunner(context.Background(), test.input)
if err != nil {
t.Fatalf("failed to get runner: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_DeleteRunner(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
if err := testDatastore.CreateRunner(context.Background(), datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}); err != nil {
t.Fatalf("failed to create runner: %+v", err)
}
deleted := datastore.Runner{
UUID: testRunnerID,
ShoesType: "shoes-test",
TargetID: testTargetID,
CloudID: "mycloud-uuid",
ResourceType: datastore.ResourceTypeNano,
RepositoryURL: "https://github.com/octocat/Hello-World",
RequestWebhook: "{}",
}
tests := []struct {
input uuid.UUID
want *datastore.Runner
err bool
}{
{
input: testRunnerID,
want: &deleted,
err: false,
},
}
for _, test := range tests {
err := testDatastore.DeleteRunner(context.Background(), test.input, time.Now().UTC(), datastore.RunnerStatusCompleted)
if !test.err && err != nil {
t.Fatalf("failed to create target: %+v", err)
}
got, err := getRunnerFromSQL(testDB, test.input)
if err != nil {
t.Fatalf("failed to get target from SQL: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
got.DeletedAt = sql.NullTime{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
if _, err := getRunningRunnerFromSQL(testDB, test.input); err == nil || errors.Is(err, sql.ErrNoRows) {
t.Errorf("%s is deleted, but exist in runner_running: %+v", test.input, err)
}
if _, err := getDeletedRunnerFromSQL(testDB, test.input); err != nil {
t.Fatalf("%s is not exist in runners_deleted: %+v", test.input, err)
}
}
}
func getRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {
var r datastore.Runner
query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE runner_id = ?`
stmt, err := testDB.Preparex(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare: %w", err)
}
err = stmt.Get(&r, id)
if err != nil {
return nil, fmt.Errorf("failed to get runner: %w", err)
}
return &r, nil
}
func getRunningRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {
var r datastore.Runner
query := `SELECT detail.runner_id, shoes_type, ip_address, target_id, cloud_id, detail.created_at, updated_at, detail.resource_type, detail.repository_url, detail.request_webhook
FROM runner_detail AS detail JOIN runnesr_running AS running ON detail.runner_id = running.runner_id WHERE detail.runner_id = ?`
stmt, err := testDB.Preparex(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare: %w", err)
}
err = stmt.Get(&r, id)
if err != nil {
return nil, fmt.Errorf("failed to get runner: %w", err)
}
return &r, nil
}
func getDeletedRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {
var r datastore.Runner
query := `SELECT detail.runner_id, shoes_type, ip_address, target_id, cloud_id, detail.created_at, updated_at, detail.resource_type, detail.repository_url, detail.request_webhook
FROM runner_detail AS detail JOIN runners_deleted AS deleted ON detail.runner_id = deleted.runner_id WHERE detail.runner_id = ?`
stmt, err := testDB.Preparex(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare: %w", err)
}
err = stmt.Get(&r, id)
if err != nil {
return nil, fmt.Errorf("failed to get runner: %w", err)
}
return &r, nil
}
================================================
FILE: pkg/datastore/mysql/schema.sql
================================================
CREATE TABLE `targets` (
`uuid` VARCHAR(36) NOT NULL PRIMARY KEY,
`scope` VARCHAR(255) NOT NULL,
`ghe_domain` VARCHAR(255),
`github_token` VARCHAR(255) NOT NULL,
`token_expired_at` TIMESTAMP NOT NULL,
`resource_type` ENUM('nano', 'micro', 'small', 'medium', 'large', 'xlarge', '2xlarge', '3xlarge', '4xlarge') NOT NULL,
`provider_url` VARCHAR(255),
`status` VARCHAR(255) NOT NULL DEFAULT 'active',
`status_description` VARCHAR(255),
`created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
`updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,
UNIQUE KEY `ghe_domain_scope` (`ghe_domain`, `scope`)
);
CREATE TABLE `runners` (
`uuid` VARCHAR(36) NOT NULL PRIMARY KEY,
`created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp
);
CREATE TABLE `runner_detail` (
`runner_id` VARCHAR(36) NOT NULL,
`shoes_type` VARCHAR(255) NOT NULL,
`ip_address` VARCHAR(255) NOT NULL,
`target_id` VARCHAR(36) NOT NULL,
`cloud_id` TEXT NOT NULL,
`resource_type` ENUM('nano', 'micro', 'small', 'medium', 'large', 'xlarge', '2xlarge', '3xlarge', '4xlarge') NOT NULL,
`runner_user` VARCHAR(255),
`provider_url` VARCHAR(255),
`repository_url` VARCHAR(255) NOT NULL,
`request_webhook` TEXT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
`updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,
KEY `fk_runner_target_id` (`target_id`),
CONSTRAINT `runners_ibfk_1` FOREIGN KEY fk_runner_target_id(`target_id`) REFERENCES targets(`uuid`) ON DELETE RESTRICT,
KEY `fk_runner_detail_id` (`runner_id`),
CONSTRAINT `runners_ibfk_2` FOREIGN KEY fk_runner_detail_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE RESTRICT
);
CREATE TABLE `runners_running` (
`runner_id` VARCHAR(36) NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
KEY `fk_runner_deleted_id` (`runner_id`),
CONSTRAINT `runners_running_ibfk_1` FOREIGN KEY fk_runner_deleted_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE CASCADE
);
CREATE TABLE `runners_deleted` (
`runner_id` VARCHAR(36) NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
`reason` VARCHAR(255) NOT NULL,
KEY `fk_runner_deleted_id` (`runner_id`),
CONSTRAINT `runners_deleted_ibfk_1` FOREIGN KEY fk_runner_deleted_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE CASCADE
);
CREATE TABLE `jobs` (
`uuid` VARCHAR(36) NOT NULL PRIMARY KEY,
`ghe_domain` VARCHAR(255),
`repository` VARCHAR(255) NOT NULL,
`check_event` TEXT NOT NULL,
`target_id` VARCHAR(36) NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
`updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,
KEY `fk_job_target_id` (`target_id`),
CONSTRAINT `jobs_ibfk_1` FOREIGN KEY fk_job_target_id(`target_id`) REFERENCES targets(`uuid`) ON DELETE RESTRICT
);
================================================
FILE: pkg/datastore/mysql/target.go
================================================
package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/pkg/datastore"
)
// CreateTarget create a target
func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Target) error {
expiredAtRFC3339 := target.TokenExpiredAt.Format("2006-01-02 15:04:05")
query := `INSERT INTO targets(uuid, scope, ghe_domain, github_token, token_expired_at, resource_type, provider_url) VALUES (?, ?, ?, ?, ?, ?, ?)`
if _, err := m.Conn.ExecContext(
ctx,
query,
target.UUID,
target.Scope,
target.GHEDomain,
target.GitHubToken,
expiredAtRFC3339,
target.ResourceType,
target.ProviderURL,
); err != nil {
return fmt.Errorf("failed to execute INSERT query: %w", err)
}
return nil
}
// GetTarget get a target
func (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) {
var t datastore.Target
query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?`
if err := m.Conn.GetContext(ctx, &t, query, id.String()); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return &t, nil
}
// GetTargetByScope get a target from scope
func (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) {
var t datastore.Target
query := fmt.Sprintf(`SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE scope = "%s"`, scope)
if err := m.Conn.GetContext(ctx, &t, query); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, datastore.ErrNotFound
}
return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
}
return &t, nil
}
// ListTargets get a all target
func (m *MySQL) ListTargets(ctx context.Context) ([]datastore.Target, error) {
var ts []datastore.Target
query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets`
if err := m.Conn.SelectContext(ctx, &ts, query); err != nil {
return nil, fmt.Errorf("failed to SELECT query: %w", err)
}
return ts, nil
}
// DeleteTarget delete a target
func (m *MySQL) DeleteTarget(ctx context.Context, id uuid.UUID) error {
query := `UPDATE targets SET status = "deleted" WHERE uuid = ?`
if _, err := m.Conn.ExecContext(ctx, query, id.String()); err != nil {
return fmt.Errorf("failed to execute DELETE query: %w", err)
}
return nil
}
// UpdateTargetStatus update status in target
func (m *MySQL) UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus datastore.TargetStatus, description string) error {
query := `UPDATE targets SET status = ?, status_description = ? WHERE uuid = ?`
if _, err := m.Conn.ExecContext(ctx, query, newStatus, description, targetID.String()); err != nil {
return fmt.Errorf("failed to execute UPDATE query: %w", err)
}
return nil
}
// UpdateToken update token in target
func (m *MySQL) UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error {
query := `UPDATE targets SET github_token = ?, token_expired_at = ? WHERE uuid = ?`
if _, err := m.Conn.ExecContext(ctx, query, newToken, newExpiredAt, targetID.String()); err != nil {
return fmt.Errorf("failed to execute UPDATE query: %w", err)
}
return nil
}
// UpdateTargetParam update parameter of target
func (m *MySQL) UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType datastore.ResourceType, newProviderURL sql.NullString) error {
query := `UPDATE targets SET resource_type = ?, provider_url = ? WHERE uuid = ?`
if _, err := m.Conn.ExecContext(ctx, query, newResourceType, newProviderURL, targetID.String()); err != nil {
return fmt.Errorf("failed to execute UPDATE query: %w", err)
}
return nil
}
================================================
FILE: pkg/datastore/mysql/target_test.go
================================================
package mysql_test
import (
"context"
"database/sql"
"errors"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/jmoiron/sqlx"
uuid "github.com/satori/go.uuid"
"github.com/whywaita/myshoes/internal/testutils"
"github.com/whywaita/myshoes/pkg/datastore"
)
var testTargetID = uuid.FromStringOrNil("8a72d42c-372c-4e0d-9c6a-4304d44af137")
var testTargetID2 = uuid.FromStringOrNil("d14ccfea-b123-4ada-974e-bbff0937e9c7")
var testScopeOrg = "octocat"
var testScopeRepo = "octocat/hello-world"
var testScopeRepo2 = "octocat/hello-world2"
var testGitHubToken = "this-code-is-github-token"
var testRunnerUser = "testing-super-user"
var testProviderURL = "/shoes-mock"
var testTime = time.Date(2037, 9, 3, 0, 0, 0, 0, time.UTC)
func TestMySQL_CreateTarget(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
tests := []struct {
input datastore.Target
want *datastore.Target
err bool
}{
{
input: datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
want: &datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
err: false,
},
{
input: datastore.Target{
UUID: testTargetID2,
Scope: testScopeRepo2,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
GHEDomain: sql.NullString{
String: "https://example.com",
Valid: true,
},
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
want: &datastore.Target{
UUID: testTargetID2,
Scope: testScopeRepo2,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
GHEDomain: sql.NullString{
String: "https://example.com",
Valid: true,
},
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
err: false,
},
}
for _, test := range tests {
err := testDatastore.CreateTarget(context.Background(), test.input)
if !test.err && err != nil {
t.Fatalf("failed to create target: %+v", err)
}
got, err := getTargetFromSQL(testDB, test.input.UUID)
if err != nil {
t.Fatalf("failed to get target from SQL: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_GetTarget(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
})
if err != nil {
t.Fatalf("failed to create target: %+v", err)
}
tests := []struct {
input uuid.UUID
want *datastore.Target
err bool
}{
{
input: testTargetID,
want: &datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
err: false,
},
}
for _, test := range tests {
got, err := testDatastore.GetTarget(context.Background(), test.input)
if err != nil {
t.Fatalf("failed to get target: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_GetTargetByScope(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
tests := []struct {
input string
want *datastore.Target
prepare func() error
err bool
}{
{
// create single instance
input: testScopeRepo,
want: &datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
prepare: func() error {
return testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
})
},
err: false,
},
{
// repository is active and organization is deleted, correct return repository
input: testScopeRepo,
want: &datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
prepare: func() error {
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
return fmt.Errorf("failed to create repository: %w", err)
}
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID2,
Scope: testScopeOrg,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
return fmt.Errorf("failed to create organization (will delete): %w", err)
}
if err := testDatastore.DeleteTarget(context.Background(), testTargetID2); err != nil {
return fmt.Errorf("failed to delete organization: %w", err)
}
return nil
},
err: false,
},
{
// repository is deleted and organization is active, correct return organization
input: testScopeOrg,
want: &datastore.Target{
UUID: testTargetID2,
Scope: testScopeOrg,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
prepare: func() error {
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
return fmt.Errorf("failed to create repository (will delete): %w", err)
}
if err := testDatastore.DeleteTarget(context.Background(), testTargetID); err != nil {
return fmt.Errorf("failed to delete repository: %w", err)
}
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID2,
Scope: testScopeOrg,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
return fmt.Errorf("failed to create deleted organization: %w", err)
}
return nil
},
err: false,
},
}
for _, test := range tests {
if err := test.prepare(); err != nil {
t.Fatalf("failed to prepare function: %+v", err)
}
got, err := testDatastore.GetTargetByScope(context.Background(), test.input)
if err != nil {
t.Fatalf("failed to get target: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
teardown()
}
}
func TestMySQL_ListTargets(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
tests := []struct {
input interface{}
want []datastore.Target
err bool
}{
{
input: nil,
want: []datastore.Target{
{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
Status: datastore.TargetStatusActive,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
},
err: false,
},
}
for _, test := range tests {
got, err := testDatastore.ListTargets(context.Background())
if !test.err && err != nil {
t.Fatalf("failed to list targets: %+v", err)
}
for i := range got {
got[i].CreatedAt = time.Time{}
got[i].UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_DeleteTarget(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
tests := []struct {
input uuid.UUID
want *datastore.Target
err bool
}{
{
input: testTargetID,
want: &datastore.Target{
UUID: testTargetID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
Status: datastore.TargetStatusDeleted,
},
err: false,
},
}
for _, test := range tests {
err := testDatastore.DeleteTarget(context.Background(), test.input)
if !test.err && err != nil {
t.Fatalf("failed to delete target: %+v", err)
}
got, err := getTargetFromSQL(testDB, test.input)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("failed to get target from SQL: %+v", err)
}
if got != nil {
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
func TestMySQL_UpdateStatus(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
type Input struct {
status datastore.TargetStatus
description string
}
tests := []struct {
input Input
want *datastore.Target
err bool
}{
{
input: Input{
status: datastore.TargetStatusActive,
description: "",
},
want: &datastore.Target{
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
Status: datastore.TargetStatusActive,
StatusDescription: sql.NullString{
String: "",
Valid: true,
},
},
err: false,
},
{
input: Input{
status: datastore.TargetStatusRunning,
description: "job-id",
},
want: &datastore.Target{
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
Status: datastore.TargetStatusRunning,
StatusDescription: sql.NullString{
String: "job-id",
Valid: true,
},
},
err: false,
},
}
for _, test := range tests {
tID := uuid.NewV4()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: tID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
//lint:ignore SA1019 only execute in test
err := testDatastore.UpdateTargetStatus(context.Background(), tID, test.input.status, test.input.description)
if !test.err && err != nil {
t.Fatalf("failed to update status: %+v", err)
}
got, err := getTargetFromSQL(testDB, tID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("failed to get target from SQL: %+v", err)
}
if got != nil {
got.UUID = uuid.UUID{}
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
if err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {
t.Fatalf("failed to delete target: %+v", err)
}
}
}
func TestMySQL_UpdateToken(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
type Input struct {
token string
expired time.Time
}
tests := []struct {
input Input
want *datastore.Target
err bool
}{
{
input: Input{
token: "new-token",
expired: testTime.Add(1 * time.Hour),
},
want: &datastore.Target{
Scope: testScopeRepo,
GitHubToken: "new-token",
TokenExpiredAt: testTime.Add(1 * time.Hour),
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
Status: datastore.TargetStatusActive,
StatusDescription: sql.NullString{
String: "",
Valid: false,
},
},
err: false,
},
}
for _, test := range tests {
tID := uuid.NewV4()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: tID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
err := testDatastore.UpdateToken(context.Background(), tID, test.input.token, test.input.expired)
if !test.err && err != nil {
t.Fatalf("failed to update status: %+v", err)
}
got, err := getTargetFromSQL(testDB, tID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("failed to get target from SQL: %+v", err)
}
if got != nil {
got.UUID = uuid.UUID{}
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
if err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {
t.Fatalf("failed to delete target: %+v", err)
}
}
}
func TestMySQL_UpdateTargetParam(t *testing.T) {
testDatastore, teardown := testutils.GetTestDatastore()
defer teardown()
testDB, _ := testutils.GetTestDB()
type input struct {
resourceType datastore.ResourceType
runnerUser sql.NullString
providerURL sql.NullString
}
tests := []struct {
input input
want *datastore.Target
err bool
}{
{
input: input{
resourceType: datastore.ResourceTypeLarge,
runnerUser: sql.NullString{
String: "",
Valid: false,
},
providerURL: sql.NullString{
String: "",
Valid: false,
},
},
want: &datastore.Target{
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
ResourceType: datastore.ResourceTypeLarge,
ProviderURL: sql.NullString{
String: "",
Valid: false,
},
Status: datastore.TargetStatusActive,
StatusDescription: sql.NullString{
String: "",
Valid: false,
},
},
err: false,
},
{
input: input{
resourceType: datastore.ResourceTypeLarge,
runnerUser: sql.NullString{
String: testRunnerUser,
Valid: true,
},
providerURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
},
want: &datastore.Target{
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
ResourceType: datastore.ResourceTypeLarge,
ProviderURL: sql.NullString{
String: testProviderURL,
Valid: true,
},
Status: datastore.TargetStatusActive,
StatusDescription: sql.NullString{
String: "",
Valid: false,
},
},
err: false,
},
{
input: input{
resourceType: datastore.ResourceTypeLarge,
runnerUser: sql.NullString{
String: testRunnerUser,
Valid: true,
},
providerURL: sql.NullString{
String: "",
Valid: false,
},
},
want: &datastore.Target{
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
ResourceType: datastore.ResourceTypeLarge,
ProviderURL: sql.NullString{
String: "",
Valid: false,
},
Status: datastore.TargetStatusActive,
StatusDescription: sql.NullString{
String: "",
Valid: false,
},
},
err: false,
},
}
for _, test := range tests {
tID := uuid.NewV4()
if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
UUID: tID,
Scope: testScopeRepo,
GitHubToken: testGitHubToken,
TokenExpiredAt: testTime,
ResourceType: datastore.ResourceTypeNano,
ProviderURL: sql.NullString{
String: "test-default-string",
Valid: true,
},
}); err != nil {
t.Fatalf("failed to create target: %+v", err)
}
if err := testDatastore.UpdateTargetParam(context.Background(), tID, test.input.resourceType, test.input.providerURL); err != nil {
t.Fatalf("failed to UpdateResourceTyoe: %+v", err)
}
got, err := getTargetFromSQL(testDB, tID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("failed to get target from SQL: %+v", err)
}
if got != nil {
got.UUID = uuid.UUID{}
got.CreatedAt = time.Time{}
got.UpdatedAt = time.Time{}
got.TokenExpiredAt = time.Time{}
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
if err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {
t.Fatalf("failed to delete target: %+v", err)
}
}
}
func getTargetFromSQL(testDB *sqlx.DB, uuid uuid.UUID) (*datastore.Target, error) {
var t datastore.Target
query := `SELECT uuid, scope, ghe_domain, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?`
stmt, err := testDB.Preparex(query)
if err != nil {
return nil, fmt.Errorf("failed to prepare: %w", err)
}
err = stmt.Get(&t, uuid)
if err != nil {
return nil, fmt.Errorf("failed to get target: %w", err)
}
return &t, nil
}
================================================
FILE: pkg/datastore/resource_type.go
================================================
package datastore
import (
"database/sql/driver"
"encoding/json"
"fmt"
pb "github.com/whywaita/myshoes/api/proto.go"
)
// ResourceType is runner machine spec
type ResourceType int
// ResourceTypes variables
const (
ResourceTypeUnknown ResourceType = iota
ResourceTypeNano
ResourceTypeMicro
ResourceTypeSmall
ResourceTypeMedium
ResourceTypeLarge
ResourceTypeXLarge
ResourceType2XLarge
ResourceType3XLarge
ResourceType4XLarge
)
// String implement interface for fmt.Stringer
func (r ResourceType) String() string {
switch r {
case ResourceTypeNano:
return "nano"
case ResourceTypeMicro:
return "micro"
case ResourceTypeSmall:
return "small"
case ResourceTypeMedium:
return "medium"
case ResourceTypeLarge:
return "large"
case ResourceTypeXLarge:
return "xlarge"
case ResourceType2XLarge:
return "2xlarge"
case ResourceType3XLarge:
return "3xlarge"
case ResourceType4XLarge:
return "4xlarge"
}
return "unknown"
}
// Value implements the database/sql/driver Valuer interface
func (r ResourceType) Value() (driver.Value, error) {
return driver.Value(r.String()), nil
}
// Scan implements the database/sql Scanner interface
func (r *ResourceType) Scan(src interface{}) error {
var rt *ResourceType
switch src := src.(type) {
case string:
unmarshaled := UnmarshalResourceType(src)
rt = &unmarshaled
case []uint8:
str := string(src)
unmarshaled := UnmarshalResourceType(str)
rt = &unmarshaled
default:
return fmt.Errorf("incompatible type for ResourceType: %T", src)
}
*r = *rt
return nil
}
// UnmarshalResourceType cast type to ResourceType
func UnmarshalResourceType(src interface{}) ResourceType {
switch src := src.(type) {
case string:
return UnmarshalResourceTypeString(src)
case pb.ResourceType:
return UnmarshalResourceTypePb(src)
}
return ResourceTypeUnknown
}
// UnmarshalResourceTypeString cast type from string to ResourceType
func UnmarshalResourceTypeString(in string) ResourceType {
switch in {
case "nano":
return ResourceTypeNano
case "micro":
return ResourceTypeMicro
case "small":
return ResourceTypeSmall
case "medium":
return ResourceTypeMedium
case "large":
return ResourceTypeLarge
case "xlarge":
return ResourceTypeXLarge
case "2xlarge":
return ResourceType2XLarge
case "3xlarge":
return ResourceType3XLarge
case "4xlarge":
return ResourceType4XLarge
}
return ResourceTypeUnknown
}
// UnmarshalResourceTypePb cast type from pb.ResourceType to ResourceType
func UnmarshalResourceTypePb(in pb.ResourceType) ResourceType {
switch in {
case pb.ResourceType_Nano:
return ResourceTypeNano
case pb.ResourceType_Micro:
return ResourceTypeMicro
case pb.ResourceType_Small:
return ResourceTypeSmall
case pb.ResourceType_Medium:
return ResourceTypeMedium
case pb.ResourceType_Large:
return ResourceTypeLarge
case pb.ResourceType_XLarge:
return ResourceTypeXLarge
case pb.ResourceType_XLarge2:
return ResourceType2XLarge
case pb.ResourceType_XLarge3:
return ResourceType3XLarge
case pb.ResourceType_XLarge4:
return ResourceType4XLarge
}
return ResourceTypeUnknown
}
// ToPb convert type of protobuf
func (r ResourceType) ToPb() pb.ResourceType {
switch r {
case ResourceTypeNano:
return pb.ResourceType_Nano
case ResourceTypeMicro:
return pb.ResourceType_Micro
case ResourceTypeSmall:
return pb.ResourceType_Small
case ResourceTypeMedium:
return pb.ResourceType_Medium
case ResourceTypeLarge:
return pb.ResourceType_Large
case ResourceTypeXLarge:
return pb.ResourceType_XLarge
case ResourceType2XLarge:
return pb.ResourceType_XLarge2
case ResourceType3XLarge:
return pb.ResourceType_XLarge3
case ResourceType4XLarge:
return pb.ResourceType_XLarge4
}
return pb.ResourceType_Unknown
}
// MarshalJSON implements the encoding/json Marshaler interface
func (r ResourceType) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
// UnmarshalJSON implements the encoding/json Unmarshaler interface
func (r *ResourceType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("data should be a string, but got %s", data)
}
rt := UnmarshalResourceTypeString(s)
*r = rt
return nil
}
================================================
FILE: pkg/docker/ratelimit.go
================================================
package docker
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/whywaita/myshoes/pkg/config"
)
// RateLimit is Docker Hub API rate limit
type RateLimit struct {
Limit int
Remaining int
}
type tokenCache struct {
expire time.Time
token string
}
var cacheMap = make(map[int]tokenCache, 1)
func getToken() (string, error) {
url := "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
if config.Config.DockerHubCredential.Password != "" && config.Config.DockerHubCredential.Username != "" {
req.SetBasicAuth(config.Config.DockerHubCredential.Username, config.Config.DockerHubCredential.Password)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("request token: %w", err)
}
if cache, ok := cacheMap[0]; ok && cache.expire.After(time.Now()) {
return cache.token, nil
}
defer resp.Body.Close()
byteArray, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read body: %w", err)
}
jsonMap := make(map[string]interface{})
if err := json.Unmarshal(byteArray, &jsonMap); err != nil {
return "", fmt.Errorf("unmarshal json: %w", err)
}
tokenString, ok := jsonMap["token"].(string)
if !ok {
return "", fmt.Errorf("tokenString is not string")
}
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return "", fmt.Errorf("parse token: %w", err)
}
exp, ok := token.Claims.(jwt.MapClaims)["exp"].(float64)
if !ok {
return "", fmt.Errorf("exp is not float64")
}
cacheMap[0] = tokenCache{
expire: time.Unix(int64(exp), 0),
token: tokenString,
}
return tokenString, nil
}
// GetRateLimit get Docker Hub API rate limit
func GetRateLimit() (RateLimit, error) {
token, err := getToken()
if err != nil {
return RateLimit{}, fmt.Errorf("get token: %w", err)
}
url := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return RateLimit{}, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return RateLimit{}, fmt.Errorf("get rate limit: %w", err)
}
defer resp.Body.Close()
limitHeader := resp.Header.Get("ratelimit-limit")
if limitHeader == "" {
return RateLimit{}, fmt.Errorf("not found ratelimit-limit header")
}
limit, err := strconv.Atoi(strings.Split(limitHeader, ";")[0])
if err != nil {
return RateLimit{}, fmt.Errorf("parse limit: %w", err)
}
remainingHeader := resp.Header.Get("ratelimit-remaining")
if remainingHeader == "" {
return RateLimit{}, fmt.Errorf("not found ratelimit-remaining header")
}
remaining, err := strconv.Atoi(strings.Split(remainingHeader, ";")[0])
if err != nil {
return RateLimit{}, fmt.Errorf("parse remaining: %w", err)
}
return RateLimit{
Limit: limit,
Remaining: remaining,
}, nil
}
================================================
FILE: pkg/gh/github.go
================================================
package gh
import (
"fmt"
"net/http"
"net/url"
"path"
"sync"
"time"
"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v80/github"
"github.com/m4ns0ur/httpcache"
"github.com/patrickmn/go-cache"
"github.com/whywaita/myshoes/pkg/config"
"golang.org/x/oauth2"
)
var (
// ErrNotFound is error for not found
ErrNotFound = fmt.Errorf("not found")
// ResponseCache is cache variable
responseCache = cache.New(5*time.Minute, 10*time.Minute)
// rateLimitRemain is remaining of Rate limit, for metrics
rateLimitRemain = sync.Map{}
// rateLimitLimit is limit of Rate limit, for metrics
rateLimitLimit = sync.Map{}
// httpCache is shareable response cache
httpCache = httpcache.NewMemoryCache()
// appTransport is transport for GitHub Apps
appTransport = ghinstallation.AppsTransport{}
// installationTransports is map of ghinstallation.Transport for cache token of installation.
// key: installationID, value: ghinstallation.Transport
installationTransports = sync.Map{}
)
// InitializeCache create a cache
func InitializeCache(appID int64, appPEM []byte) error {
tr := httpcache.NewTransport(httpCache)
itr, err := ghinstallation.NewAppsTransport(tr, appID, appPEM)
if err != nil {
return fmt.Errorf("failed to create Apps transport: %w", err)
}
appTransport = *itr
return nil
}
// NewClient create a client of GitHub
func NewClient(token string) (*github.Client, error) {
oauth2Transport := &oauth2.Transport{
Source: oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
),
}
transport := &httpcache.Transport{
Transport: oauth2Transport,
Cache: httpCache,
MarkCachedResponses: true,
}
clientTransport := newInstrumentedTransport(transport)
if !config.Config.IsGHES() {
return github.NewClient(&http.Client{Transport: clientTransport}), nil
}
return github.NewClient(&http.Client{Transport: clientTransport}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)
}
// NewClientGitHubApps create a client of GitHub using Private Key from GitHub Apps
// header is "Authorization: Bearer YOUR_JWT"
// docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app
func NewClientGitHubApps() (*github.Client, error) {
if !config.Config.IsGHES() {
return github.NewClient(&http.Client{Transport: newInstrumentedTransport(&appTransport)}), nil
}
apiEndpoint, err := getAPIEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get GitHub API Endpoint: %w", err)
}
itr := appTransport
itr.BaseURL = apiEndpoint.String()
return github.NewClient(&http.Client{Transport: newInstrumentedTransport(&appTransport)}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)
}
// NewClientInstallation create a client of GitHub using installation ID from GitHub Apps
// header is "Authorization: token YOUR_INSTALLATION_ACCESS_TOKEN"
// docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-an-installation
func NewClientInstallation(installationID int64) (*github.Client, error) {
itr := getInstallationTransport(installationID)
if !config.Config.IsGHES() {
return github.NewClient(&http.Client{Transport: newInstrumentedTransport(itr)}), nil
}
apiEndpoint, err := getAPIEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get GitHub API Endpoint: %w", err)
}
itr.BaseURL = apiEndpoint.String()
return github.NewClient(&http.Client{Transport: newInstrumentedTransport(itr)}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)
}
func setInstallationTransport(installationID int64, itr ghinstallation.Transport) {
installationTransports.Store(installationID, itr)
}
func getInstallationTransport(installationID int64) *ghinstallation.Transport {
got, found := installationTransports.Load(installationID)
if !found {
return generateInstallationTransport(installationID)
}
itr, ok := got.(ghinstallation.Transport)
if !ok {
return generateInstallationTransport(installationID)
}
return &itr
}
func generateInstallationTransport(installationID int64) *ghinstallation.Transport {
itr := ghinstallation.NewFromAppsTransport(&appTransport, installationID)
setInstallationTransport(installationID, *itr)
return itr
}
// CheckSignature check trust installation id from event.
func CheckSignature(installationID int64) error {
if itr := ghinstallation.NewFromAppsTransport(&appTransport, installationID); itr == nil {
return fmt.Errorf("failed to create GitHub installation")
}
return nil
}
// ExistRunnerReleases check exist of runner file
func ExistRunnerReleases(runnerVersion string) error {
releasesURL := fmt.Sprintf("https://github.com/actions/runner/releases/tag/%s", runnerVersion)
resp, err := http.Get(releasesURL)
if err != nil {
return fmt.Errorf("failed to GET from %s: %w", releasesURL, ErrNotFound)
}
if resp.StatusCode == http.StatusOK {
return nil
} else if resp.StatusCode == http.StatusNotFound {
return ErrNotFound
}
return fmt.Errorf("invalid response code (%d)", resp.StatusCode)
}
// ExistGitHubRepository check exist of GitHub repository
func ExistGitHubRepository(scope string, accessToken string) error {
repoURL, err := getRepositoryURL(scope)
if err != nil {
return fmt.Errorf("failed to get repository url: %w", err)
}
client := &http.Client{Transport: newInstrumentedTransport(http.DefaultTransport)}
req, err := http.NewRequest(http.Meth
gitextract_98bm4mpl/
├── .github/
│ └── workflows/
│ ├── build-docker-sha.yaml
│ ├── release.yaml
│ └── test.yaml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api/
│ ├── myshoes/
│ │ ├── README.md
│ │ ├── client.go
│ │ ├── http.go
│ │ └── target.go
│ ├── proto/
│ │ └── myshoes.proto
│ └── proto.go/
│ ├── myshoes.pb.go
│ └── myshoes_grpc.pb.go
├── cmd/
│ ├── server/
│ │ └── cmd.go
│ └── shoes-tester/
│ └── main.go
├── docs/
│ ├── 01_01_for_admin_setup.md
│ ├── 01_02_for_admin_tips.md
│ ├── 02_01_for_user_setup.md
│ ├── 03_how-to-develop-shoes.md
│ └── assets/
│ └── myshoes.service
├── go.mod
├── go.sum
├── internal/
│ ├── testutils/
│ │ ├── mysql.go
│ │ ├── testutils.go
│ │ └── web.go
│ └── util/
│ └── util.go
└── pkg/
├── config/
│ ├── config.go
│ └── init.go
├── datastore/
│ ├── github.go
│ ├── interface.go
│ ├── memory/
│ │ └── memory.go
│ ├── mysql/
│ │ ├── job.go
│ │ ├── job_test.go
│ │ ├── lock.go
│ │ ├── mysql.go
│ │ ├── mysql_test.go
│ │ ├── runner.go
│ │ ├── runner_test.go
│ │ ├── schema.sql
│ │ ├── target.go
│ │ └── target_test.go
│ └── resource_type.go
├── docker/
│ └── ratelimit.go
├── gh/
│ ├── github.go
│ ├── github_test.go
│ ├── installation.go
│ ├── jwt.go
│ ├── jwt_test.go
│ ├── label.go
│ ├── metrics.go
│ ├── metrics_test.go
│ ├── ratelimit.go
│ ├── runner.go
│ ├── scope.go
│ ├── token_registration.go
│ ├── webhook.go
│ ├── workflow_job.go
│ └── workflow_run.go
├── logger/
│ └── logger.go
├── metric/
│ ├── collector.go
│ ├── scrape_datastore.go
│ ├── scrape_github.go
│ ├── scrape_memory.go
│ └── webhook.go
├── runner/
│ ├── metrics.go
│ ├── runner.go
│ ├── runner_delete.go
│ ├── runner_delete_ephemeral.go
│ ├── runner_delete_once.go
│ ├── token_update.go
│ └── util.go
├── shoes/
│ └── shoes.go
├── starter/
│ ├── README.md
│ ├── error.go
│ ├── metric.go
│ ├── metrics.go
│ ├── safety/
│ │ ├── README.md
│ │ ├── safety.go
│ │ └── unlimited/
│ │ └── unlimited.go
│ ├── scripts/
│ │ └── RunnerService.js
│ ├── scripts.go
│ └── starter.go
└── web/
├── config.go
├── http.go
├── http_test.go
├── metrics.go
├── target.go
├── target_create.go
├── target_test.go
└── webhook.go
SYMBOL INDEX (505 symbols across 69 files)
FILE: api/myshoes/client.go
type Client (line 15) | type Client struct
method newRequest (line 57) | func (c *Client) newRequest(ctx context.Context, method, spath string,...
constant defaultUserAgent (line 24) | defaultUserAgent = "myshoes-sdk-go"
function NewClient (line 28) | func NewClient(endpoint string, client *http.Client, logger *log.Logger)...
FILE: api/myshoes/http.go
function decodeBody (line 12) | func decodeBody(resp *http.Response, out interface{}) error {
function decodeErrorBody (line 26) | func decodeErrorBody(resp *http.Response) error {
method request (line 36) | func (c *Client) request(req *http.Request, out interface{}) error {
FILE: api/myshoes/target.go
method CreateTarget (line 14) | func (c *Client) CreateTarget(ctx context.Context, param web.TargetCreat...
method GetTarget (line 36) | func (c *Client) GetTarget(ctx context.Context, targetID string) (*web.U...
method UpdateTarget (line 53) | func (c *Client) UpdateTarget(ctx context.Context, targetID string, para...
method DeleteTarget (line 75) | func (c *Client) DeleteTarget(ctx context.Context, targetID string) error {
method ListTarget (line 92) | func (c *Client) ListTarget(ctx context.Context) ([]web.UserTarget, erro...
FILE: api/proto.go/myshoes.pb.go
constant _ (line 19) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
constant _ (line 21) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
type ResourceType (line 24) | type ResourceType
method Enum (line 67) | func (x ResourceType) Enum() *ResourceType {
method String (line 73) | func (x ResourceType) String() string {
method Descriptor (line 77) | func (ResourceType) Descriptor() protoreflect.EnumDescriptor {
method Type (line 81) | func (ResourceType) Type() protoreflect.EnumType {
method Number (line 85) | func (x ResourceType) Number() protoreflect.EnumNumber {
method EnumDescriptor (line 90) | func (ResourceType) EnumDescriptor() ([]byte, []int) {
constant ResourceType_Unknown (line 27) | ResourceType_Unknown ResourceType = 0
constant ResourceType_Nano (line 28) | ResourceType_Nano ResourceType = 1
constant ResourceType_Micro (line 29) | ResourceType_Micro ResourceType = 2
constant ResourceType_Small (line 30) | ResourceType_Small ResourceType = 3
constant ResourceType_Medium (line 31) | ResourceType_Medium ResourceType = 4
constant ResourceType_Large (line 32) | ResourceType_Large ResourceType = 5
constant ResourceType_XLarge (line 33) | ResourceType_XLarge ResourceType = 6
constant ResourceType_XLarge2 (line 34) | ResourceType_XLarge2 ResourceType = 7
constant ResourceType_XLarge3 (line 35) | ResourceType_XLarge3 ResourceType = 8
constant ResourceType_XLarge4 (line 36) | ResourceType_XLarge4 ResourceType = 9
type AddInstanceRequest (line 94) | type AddInstanceRequest struct
method Reset (line 104) | func (x *AddInstanceRequest) Reset() {
method String (line 111) | func (x *AddInstanceRequest) String() string {
method ProtoMessage (line 115) | func (*AddInstanceRequest) ProtoMessage() {}
method ProtoReflect (line 117) | func (x *AddInstanceRequest) ProtoReflect() protoreflect.Message {
method Descriptor (line 130) | func (*AddInstanceRequest) Descriptor() ([]byte, []int) {
method GetRunnerName (line 134) | func (x *AddInstanceRequest) GetRunnerName() string {
method GetSetupScript (line 141) | func (x *AddInstanceRequest) GetSetupScript() string {
method GetResourceType (line 148) | func (x *AddInstanceRequest) GetResourceType() ResourceType {
method GetLabels (line 155) | func (x *AddInstanceRequest) GetLabels() []string {
type AddInstanceResponse (line 162) | type AddInstanceResponse struct
method Reset (line 172) | func (x *AddInstanceResponse) Reset() {
method String (line 179) | func (x *AddInstanceResponse) String() string {
method ProtoMessage (line 183) | func (*AddInstanceResponse) ProtoMessage() {}
method ProtoReflect (line 185) | func (x *AddInstanceResponse) ProtoReflect() protoreflect.Message {
method Descriptor (line 198) | func (*AddInstanceResponse) Descriptor() ([]byte, []int) {
method GetCloudId (line 202) | func (x *AddInstanceResponse) GetCloudId() string {
method GetShoesType (line 209) | func (x *AddInstanceResponse) GetShoesType() string {
method GetIpAddress (line 216) | func (x *AddInstanceResponse) GetIpAddress() string {
method GetResourceType (line 223) | func (x *AddInstanceResponse) GetResourceType() ResourceType {
type DeleteInstanceRequest (line 230) | type DeleteInstanceRequest struct
method Reset (line 238) | func (x *DeleteInstanceRequest) Reset() {
method String (line 245) | func (x *DeleteInstanceRequest) String() string {
method ProtoMessage (line 249) | func (*DeleteInstanceRequest) ProtoMessage() {}
method ProtoReflect (line 251) | func (x *DeleteInstanceRequest) ProtoReflect() protoreflect.Message {
method Descriptor (line 264) | func (*DeleteInstanceRequest) Descriptor() ([]byte, []int) {
method GetCloudId (line 268) | func (x *DeleteInstanceRequest) GetCloudId() string {
method GetLabels (line 275) | func (x *DeleteInstanceRequest) GetLabels() []string {
type DeleteInstanceResponse (line 282) | type DeleteInstanceResponse struct
method Reset (line 288) | func (x *DeleteInstanceResponse) Reset() {
method String (line 295) | func (x *DeleteInstanceResponse) String() string {
method ProtoMessage (line 299) | func (*DeleteInstanceResponse) ProtoMessage() {}
method ProtoReflect (line 301) | func (x *DeleteInstanceResponse) ProtoReflect() protoreflect.Message {
method Descriptor (line 314) | func (*DeleteInstanceResponse) Descriptor() ([]byte, []int) {
constant file_myshoes_proto_rawDesc (line 320) | file_myshoes_proto_rawDesc = "" +
function file_myshoes_proto_rawDescGZIP (line 362) | func file_myshoes_proto_rawDescGZIP() []byte {
function init (line 392) | func init() { file_myshoes_proto_init() }
function file_myshoes_proto_init (line 393) | func file_myshoes_proto_init() {
FILE: api/proto.go/myshoes_grpc.pb.go
constant _ (line 19) | _ = grpc.SupportPackageIsVersion9
constant Shoes_AddInstance_FullMethodName (line 22) | Shoes_AddInstance_FullMethodName = "/whywaita.myshoes.Shoes/AddInstance"
constant Shoes_DeleteInstance_FullMethodName (line 23) | Shoes_DeleteInstance_FullMethodName = "/whywaita.myshoes.Shoes/DeleteIns...
type ShoesClient (line 29) | type ShoesClient interface
type shoesClient (line 34) | type shoesClient struct
method AddInstance (line 42) | func (c *shoesClient) AddInstance(ctx context.Context, in *AddInstance...
method DeleteInstance (line 52) | func (c *shoesClient) DeleteInstance(ctx context.Context, in *DeleteIn...
function NewShoesClient (line 38) | func NewShoesClient(cc grpc.ClientConnInterface) ShoesClient {
type ShoesServer (line 65) | type ShoesServer interface
type UnimplementedShoesServer (line 76) | type UnimplementedShoesServer struct
method AddInstance (line 78) | func (UnimplementedShoesServer) AddInstance(context.Context, *AddInsta...
method DeleteInstance (line 81) | func (UnimplementedShoesServer) DeleteInstance(context.Context, *Delet...
method mustEmbedUnimplementedShoesServer (line 84) | func (UnimplementedShoesServer) mustEmbedUnimplementedShoesServer() {}
method testEmbeddedByValue (line 85) | func (UnimplementedShoesServer) testEmbeddedByValue() {}
type UnsafeShoesServer (line 90) | type UnsafeShoesServer interface
function RegisterShoesServer (line 94) | func RegisterShoesServer(s grpc.ServiceRegistrar, srv ShoesServer) {
function _Shoes_AddInstance_Handler (line 105) | func _Shoes_AddInstance_Handler(srv interface{}, ctx context.Context, de...
function _Shoes_DeleteInstance_Handler (line 123) | func _Shoes_DeleteInstance_Handler(srv interface{}, ctx context.Context,...
FILE: cmd/server/cmd.go
function init (line 25) | func init() {
function main (line 35) | func main() {
type myShoes (line 52) | type myShoes struct
method Run (line 80) | func (m *myShoes) Run() error {
function newShoes (line 59) | func newShoes() (*myShoes, error) {
FILE: cmd/shoes-tester/main.go
function main (line 23) | func main() {
function printUsage (line 48) | func printUsage() {
type addFlags (line 59) | type addFlags struct
type deleteFlags (line 76) | type deleteFlags struct
function runAdd (line 83) | func runAdd(args []string) error {
function runDelete (line 177) | func runDelete(args []string) error {
function getClientWithPath (line 225) | func getClientWithPath(pluginPath string) (shoes.Client, func(), error) {
function generateSetupScript (line 259) | func generateSetupScript(ctx context.Context, flags *addFlags) (string, ...
function parseLabels (line 301) | func parseLabels(labels string) []string {
FILE: internal/testutils/mysql.go
constant schemaDirRelativePathFormat (line 16) | schemaDirRelativePathFormat = "%s/../../pkg/datastore/mysql/%s"
function execSchema (line 18) | func execSchema(fpath string) {
function createTablesIfNotExist (line 34) | func createTablesIfNotExist() {
function truncateTables (line 40) | func truncateTables() {
function GetTestDatastore (line 81) | func GetTestDatastore() (datastore.Datastore, func()) {
function GetTestDB (line 90) | func GetTestDB() (*sqlx.DB, func()) {
FILE: internal/testutils/testutils.go
constant mysqlRootPassword (line 18) | mysqlRootPassword = "secret"
function IntegrationTestRunner (line 29) | func IntegrationTestRunner(m *testing.M) int {
FILE: internal/testutils/web.go
function GetTestURL (line 4) | func GetTestURL() string {
FILE: internal/util/util.go
function CalcRetryTime (line 9) | func CalcRetryTime(count int) time.Duration {
FILE: pkg/config/config.go
type Conf (line 12) | type Conf struct
method IsGHES (line 122) | func (c Conf) IsGHES() bool {
type DockerHubCredential (line 37) | type DockerHubCredential struct
type GitHubApp (line 43) | type GitHubApp struct
constant EnvGitHubAppID (line 52) | EnvGitHubAppID = "GITHUB_APP_ID"
constant EnvGitHubAppSecret (line 53) | EnvGitHubAppSecret = "GITHUB_APP_SECRET"
constant EnvGitHubAppPrivateKeyBase64 (line 54) | EnvGitHubAppPrivateKeyBase64 = "GITHUB_PRIVATE_KEY_BASE64"
constant EnvMySQLHost (line 55) | EnvMySQLHost = "MYSQL_HOST"
constant EnvMySQLPort (line 56) | EnvMySQLPort = "MYSQL_PORT"
constant EnvMySQLUser (line 57) | EnvMySQLUser = "MYSQL_USER"
constant EnvMySQLPassword (line 58) | EnvMySQLPassword = "MYSQL_PASSWORD"
constant EnvMySQLDatabase (line 59) | EnvMySQLDatabase = "MYSQL_DATABASE"
constant EnvMySQLURL (line 60) | EnvMySQLURL = "MYSQL_URL"
constant EnvPort (line 61) | EnvPort = "PORT"
constant EnvShoesPluginPath (line 62) | EnvShoesPluginPath = "PLUGIN"
constant EnvShoesPluginOutputPath (line 63) | EnvShoesPluginOutputPath = "PLUGIN_OUTPUT"
constant EnvRunnerUser (line 64) | EnvRunnerUser = "RUNNER_USER"
constant EnvRunnerBaseDirectory (line 65) | EnvRunnerBaseDirectory = "RUNNER_BASE_DIRECTORY"
constant EnvDebug (line 66) | EnvDebug = "DEBUG"
constant EnvStrict (line 67) | EnvStrict = "STRICT"
constant EnvModeWebhookType (line 68) | EnvModeWebhookType = "MODE_WEBHOOK_TYPE"
constant EnvMaxConnectionsToBackend (line 69) | EnvMaxConnectionsToBackend = "MAX_CONNECTIONS_TO_BACKEND"
constant EnvMaxConcurrencyDeleting (line 70) | EnvMaxConcurrencyDeleting = "MAX_CONCURRENCY_DELETING"
constant EnvGitHubURL (line 71) | EnvGitHubURL = "GITHUB_URL"
constant EnvRunnerVersion (line 72) | EnvRunnerVersion = "RUNNER_VERSION"
constant EnvDockerHubUsername (line 73) | EnvDockerHubUsername = "DOCKER_HUB_USERNAME"
constant EnvDockerHubPassword (line 74) | EnvDockerHubPassword = "DOCKER_HUB_PASSWORD"
constant EnvProvideDockerHubMetrics (line 75) | EnvProvideDockerHubMetrics = "PROVIDE_DOCKER_HUB_METRICS"
type ModeWebhookType (line 79) | type ModeWebhookType
method String (line 91) | func (mwt ModeWebhookType) String() string {
method Equal (line 106) | func (mwt ModeWebhookType) Equal(in string) bool {
constant ModeWebhookTypeUnknown (line 83) | ModeWebhookTypeUnknown ModeWebhookType = iota
constant ModeWebhookTypeCheckRun (line 85) | ModeWebhookTypeCheckRun
constant ModeWebhookTypeWorkflowJob (line 87) | ModeWebhookTypeWorkflowJob
function marshalModeWebhookType (line 110) | func marshalModeWebhookType(in string) ModeWebhookType {
FILE: pkg/config/init.go
function Load (line 21) | func Load() {
function LoadWithDefault (line 34) | func LoadWithDefault() Conf {
function LoadGitHubApps (line 162) | func LoadGitHubApps() *GitHubApp {
function LoadMySQLURL (line 200) | func LoadMySQLURL() string {
function LoadPluginPath (line 219) | func LoadPluginPath() string {
function checkBinary (line 236) | func checkBinary(p string) (string, error) {
function fetch (line 267) | func fetch(p string) (string, error) {
function fetchHTTP (line 288) | func fetchHTTP(u *url.URL) (string, error) {
FILE: pkg/datastore/github.go
function NewClientInstallationByRepo (line 19) | func NewClientInstallationByRepo(ctx context.Context, ds Datastore, repo...
type PendingWorkflowRunWithTarget (line 39) | type PendingWorkflowRunWithTarget struct
function GetPendingWorkflowRunByRecentRepositories (line 45) | func GetPendingWorkflowRunByRecentRepositories(ctx context.Context, ds D...
function getPendingWorkflowRunByRecentRepositories (line 88) | func getPendingWorkflowRunByRecentRepositories(ctx context.Context, ds D...
function getPendingRunByRepo (line 135) | func getPendingRunByRepo(ctx context.Context, client *github.Client, own...
function getRecentRepositories (line 158) | func getRecentRepositories(ctx context.Context, ds Datastore) ([]string,...
FILE: pkg/datastore/interface.go
type Datastore (line 30) | type Datastore interface
type Target (line 60) | type Target struct
method OwnerRepo (line 77) | func (t *Target) OwnerRepo() (string, string) {
method CanReceiveJob (line 82) | func (t *Target) CanReceiveJob() bool {
function ListTargets (line 92) | func ListTargets(ctx context.Context, ds Datastore) ([]Target, error) {
function UpdateTargetStatus (line 110) | func UpdateTargetStatus(ctx context.Context, ds Datastore, targetID uuid...
function SearchRepo (line 131) | func SearchRepo(ctx context.Context, ds Datastore, repo string) (*Target...
type TargetStatus (line 160) | type TargetStatus
constant TargetStatusActive (line 164) | TargetStatusActive TargetStatus = "active"
constant TargetStatusRunning (line 165) | TargetStatusRunning = "running"
constant TargetStatusSuspend (line 166) | TargetStatusSuspend = "suspend"
constant TargetStatusDeleted (line 167) | TargetStatusDeleted = "deleted"
constant TargetStatusErr (line 168) | TargetStatusErr = "error"
type Job (line 172) | type Job struct
method RepoURL (line 183) | func (j *Job) RepoURL() string {
type Runner (line 200) | type Runner struct
type RunnerStatus (line 219) | type RunnerStatus
constant RunnerStatusCreated (line 223) | RunnerStatusCreated RunnerStatus = "created"
constant RunnerStatusCompleted (line 224) | RunnerStatusCompleted = "completed"
constant RunnerStatusReachHardLimit (line 225) | RunnerStatusReachHardLimit = "reach_hard_limit"
FILE: pkg/datastore/memory/memory.go
type Memory (line 16) | type Memory struct
method CreateTarget (line 39) | func (m *Memory) CreateTarget(ctx context.Context, target datastore.Ta...
method GetTarget (line 48) | func (m *Memory) GetTarget(ctx context.Context, id uuid.UUID) (*datast...
method GetTargetByScope (line 60) | func (m *Memory) GetTargetByScope(ctx context.Context, scope string) (...
method ListTargets (line 76) | func (m *Memory) ListTargets(ctx context.Context) ([]datastore.Target,...
method DeleteTarget (line 90) | func (m *Memory) DeleteTarget(ctx context.Context, id uuid.UUID) error {
method UpdateTargetStatus (line 99) | func (m *Memory) UpdateTargetStatus(ctx context.Context, targetID uuid...
method UpdateToken (line 122) | func (m *Memory) UpdateToken(ctx context.Context, targetID uuid.UUID, ...
method UpdateTargetParam (line 138) | func (m *Memory) UpdateTargetParam(ctx context.Context, targetID uuid....
method EnqueueJob (line 157) | func (m *Memory) EnqueueJob(ctx context.Context, job datastore.Job) er...
method ListJobs (line 166) | func (m *Memory) ListJobs(ctx context.Context) ([]datastore.Job, error) {
method DeleteJob (line 179) | func (m *Memory) DeleteJob(ctx context.Context, id uuid.UUID) error {
method CreateRunner (line 188) | func (m *Memory) CreateRunner(ctx context.Context, runner datastore.Ru...
method ListRunners (line 198) | func (m *Memory) ListRunners(ctx context.Context) ([]datastore.Runner,...
method ListRunnersByTargetID (line 211) | func (m *Memory) ListRunnersByTargetID(ctx context.Context, targetID u...
method ListRunnersLogBySince (line 226) | func (m *Memory) ListRunnersLogBySince(ctx context.Context, since time...
method GetRunner (line 241) | func (m *Memory) GetRunner(ctx context.Context, id uuid.UUID) (*datast...
method DeleteRunner (line 254) | func (m *Memory) DeleteRunner(ctx context.Context, id uuid.UUID, delet...
method GetLock (line 263) | func (m *Memory) GetLock(ctx context.Context) error {
method IsLocked (line 268) | func (m *Memory) IsLocked(ctx context.Context) (string, error) {
function New (line 24) | func New() (*Memory, error) {
FILE: pkg/datastore/mysql/job.go
method EnqueueJob (line 14) | func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error {
method ListJobs (line 31) | func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) {
method DeleteJob (line 46) | func (m *MySQL) DeleteJob(ctx context.Context, id uuid.UUID) error {
FILE: pkg/datastore/mysql/job_test.go
function TestMySQL_EnqueueJob (line 21) | func TestMySQL_EnqueueJob(t *testing.T) {
function TestMySQL_ListJobs (line 80) | func TestMySQL_ListJobs(t *testing.T) {
function TestMySQL_DeleteJob (line 149) | func TestMySQL_DeleteJob(t *testing.T) {
function getJobFromSQL (line 209) | func getJobFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Job, error) {
FILE: pkg/datastore/mysql/lock.go
method GetLock (line 13) | func (m *MySQL) GetLock(ctx context.Context) error {
method IsLocked (line 31) | func (m *MySQL) IsLocked(ctx context.Context) (string, error) {
FILE: pkg/datastore/mysql/mysql.go
type MySQL (line 12) | type MySQL struct
function New (line 19) | func New(dsn string, notifyEnqueueCh chan<- struct{}) (*MySQL, error) {
function getMySQLURL (line 36) | func getMySQLURL(dsn string) (string, error) {
FILE: pkg/datastore/mysql/mysql_test.go
function TestMain (line 10) | func TestMain(m *testing.M) {
FILE: pkg/datastore/mysql/runner.go
method CreateRunner (line 15) | func (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runne...
method ListRunners (line 44) | func (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, er...
method ListRunnersByTargetID (line 61) | func (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid...
method ListRunnersLogBySince (line 78) | func (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Ti...
method GetRunner (line 95) | func (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore...
method DeleteRunner (line 111) | func (m *MySQL) DeleteRunner(ctx context.Context, id uuid.UUID, deletedA...
FILE: pkg/datastore/mysql/runner_test.go
function TestMySQL_CreateRunner (line 21) | func TestMySQL_CreateRunner(t *testing.T) {
function TestMySQL_ListRunners (line 123) | func TestMySQL_ListRunners(t *testing.T) {
function TestMySQL_ListRunnersNotReturnDeleted (line 195) | func TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) {
function TestMySQL_ListRunnersLogBySince (line 262) | func TestMySQL_ListRunnersLogBySince(t *testing.T) {
function TestMySQL_GetRunner (line 326) | func TestMySQL_GetRunner(t *testing.T) {
function TestMySQL_DeleteRunner (line 388) | func TestMySQL_DeleteRunner(t *testing.T) {
function getRunnerFromSQL (line 465) | func getRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner,...
function getRunningRunnerFromSQL (line 479) | func getRunningRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore....
function getDeletedRunnerFromSQL (line 494) | func getDeletedRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore....
FILE: pkg/datastore/mysql/schema.sql
type `targets` (line 1) | CREATE TABLE `targets` (
type `runners` (line 16) | CREATE TABLE `runners` (
type `runner_detail` (line 21) | CREATE TABLE `runner_detail` (
type `runners_running` (line 40) | CREATE TABLE `runners_running` (
type `runners_deleted` (line 47) | CREATE TABLE `runners_deleted` (
type `jobs` (line 55) | CREATE TABLE `jobs` (
FILE: pkg/datastore/mysql/target.go
method CreateTarget (line 15) | func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Targe...
method GetTarget (line 37) | func (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore...
method GetTargetByScope (line 52) | func (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*da...
method ListTargets (line 67) | func (m *MySQL) ListTargets(ctx context.Context) ([]datastore.Target, er...
method DeleteTarget (line 78) | func (m *MySQL) DeleteTarget(ctx context.Context, id uuid.UUID) error {
method UpdateTargetStatus (line 88) | func (m *MySQL) UpdateTargetStatus(ctx context.Context, targetID uuid.UU...
method UpdateToken (line 98) | func (m *MySQL) UpdateToken(ctx context.Context, targetID uuid.UUID, new...
method UpdateTargetParam (line 108) | func (m *MySQL) UpdateTargetParam(ctx context.Context, targetID uuid.UUI...
FILE: pkg/datastore/mysql/target_test.go
function TestMySQL_CreateTarget (line 29) | func TestMySQL_CreateTarget(t *testing.T) {
function TestMySQL_GetTarget (line 121) | func TestMySQL_GetTarget(t *testing.T) {
function TestMySQL_GetTargetByScope (line 179) | func TestMySQL_GetTargetByScope(t *testing.T) {
function TestMySQL_ListTargets (line 347) | func TestMySQL_ListTargets(t *testing.T) {
function TestMySQL_DeleteTarget (line 406) | func TestMySQL_DeleteTarget(t *testing.T) {
function TestMySQL_UpdateStatus (line 468) | func TestMySQL_UpdateStatus(t *testing.T) {
function TestMySQL_UpdateToken (line 570) | func TestMySQL_UpdateToken(t *testing.T) {
function TestMySQL_UpdateTargetParam (line 649) | func TestMySQL_UpdateTargetParam(t *testing.T) {
function getTargetFromSQL (line 792) | func getTargetFromSQL(testDB *sqlx.DB, uuid uuid.UUID) (*datastore.Targe...
FILE: pkg/datastore/resource_type.go
type ResourceType (line 12) | type ResourceType
method String (line 29) | func (r ResourceType) String() string {
method Value (line 55) | func (r ResourceType) Value() (driver.Value, error) {
method Scan (line 60) | func (r *ResourceType) Scan(src interface{}) error {
method ToPb (line 143) | func (r ResourceType) ToPb() pb.ResourceType {
method MarshalJSON (line 169) | func (r ResourceType) MarshalJSON() ([]byte, error) {
method UnmarshalJSON (line 174) | func (r *ResourceType) UnmarshalJSON(data []byte) error {
constant ResourceTypeUnknown (line 16) | ResourceTypeUnknown ResourceType = iota
constant ResourceTypeNano (line 17) | ResourceTypeNano
constant ResourceTypeMicro (line 18) | ResourceTypeMicro
constant ResourceTypeSmall (line 19) | ResourceTypeSmall
constant ResourceTypeMedium (line 20) | ResourceTypeMedium
constant ResourceTypeLarge (line 21) | ResourceTypeLarge
constant ResourceTypeXLarge (line 22) | ResourceTypeXLarge
constant ResourceType2XLarge (line 23) | ResourceType2XLarge
constant ResourceType3XLarge (line 24) | ResourceType3XLarge
constant ResourceType4XLarge (line 25) | ResourceType4XLarge
function UnmarshalResourceType (line 79) | func UnmarshalResourceType(src interface{}) ResourceType {
function UnmarshalResourceTypeString (line 91) | func UnmarshalResourceTypeString(in string) ResourceType {
function UnmarshalResourceTypePb (line 117) | func UnmarshalResourceTypePb(in pb.ResourceType) ResourceType {
FILE: pkg/docker/ratelimit.go
type RateLimit (line 17) | type RateLimit struct
type tokenCache (line 22) | type tokenCache struct
function getToken (line 29) | func getToken() (string, error) {
function GetRateLimit (line 74) | func GetRateLimit() (RateLimit, error) {
FILE: pkg/gh/github.go
function InitializeCache (line 41) | func InitializeCache(appID int64, appPEM []byte) error {
function NewClient (line 52) | func NewClient(token string) (*github.Client, error) {
function NewClientGitHubApps (line 75) | func NewClientGitHubApps() (*github.Client, error) {
function NewClientInstallation (line 93) | func NewClientInstallation(installationID int64) (*github.Client, error) {
function setInstallationTransport (line 107) | func setInstallationTransport(installationID int64, itr ghinstallation.T...
function getInstallationTransport (line 111) | func getInstallationTransport(installationID int64) *ghinstallation.Tran...
function generateInstallationTransport (line 124) | func generateInstallationTransport(installationID int64) *ghinstallation...
function CheckSignature (line 131) | func CheckSignature(installationID int64) error {
function ExistRunnerReleases (line 140) | func ExistRunnerReleases(runnerVersion string) error {
function ExistGitHubRepository (line 157) | func ExistGitHubRepository(scope string, accessToken string) error {
function getRepositoryURL (line 184) | func getRepositoryURL(scope string) (string, error) {
function getAPIEndpoint (line 208) | func getAPIEndpoint() (*url.URL, error) {
FILE: pkg/gh/github_test.go
function TestDetectScope (line 10) | func TestDetectScope(t *testing.T) {
type TestGetRepositoryURLInput (line 42) | type TestGetRepositoryURLInput struct
function TestGetRepositoryURL (line 47) | func TestGetRepositoryURL(t *testing.T) {
FILE: pkg/gh/installation.go
function listInstallations (line 12) | func listInstallations(ctx context.Context) ([]*github.Installation, err...
function getCacheInstallationsKey (line 27) | func getCacheInstallationsKey() string {
function _listInstallations (line 31) | func _listInstallations(ctx context.Context) ([]*github.Installation, er...
function listAppsInstalledRepo (line 58) | func listAppsInstalledRepo(ctx context.Context, installationID int64) ([...
function getCacheInstalledRepoKey (line 73) | func getCacheInstalledRepoKey(installationID int64) string {
function _listAppsInstalledRepo (line 77) | func _listAppsInstalledRepo(ctx context.Context, installationID int64) (...
function GetInstallationByID (line 106) | func GetInstallationByID(ctx context.Context, installationID int64) (*gi...
function PurgeInstallationCache (line 122) | func PurgeInstallationCache(ctx context.Context) error {
FILE: pkg/gh/jwt.go
function GenerateGitHubAppsToken (line 22) | func GenerateGitHubAppsToken(ctx context.Context, clientApps *github.Cli...
function IsInstalledGitHubApp (line 34) | func IsInstalledGitHubApp(ctx context.Context, inputScope string) (int64...
type ErrIsNotInstalledGitHubApps (line 72) | type ErrIsNotInstalledGitHubApps struct
method Error (line 77) | func (e *ErrIsNotInstalledGitHubApps) Error() string {
method Unwrap (line 81) | func (e *ErrIsNotInstalledGitHubApps) Unwrap() error {
function isInstalledGitHubAppSelected (line 85) | func isInstalledGitHubAppSelected(ctx context.Context, inputScope string...
FILE: pkg/gh/jwt_test.go
function setStubFunctions (line 11) | func setStubFunctions() {
function Test_IsInstalledGitHubApp (line 62) | func Test_IsInstalledGitHubApp(t *testing.T) {
FILE: pkg/gh/label.go
function IsRequestedMyshoesLabel (line 6) | func IsRequestedMyshoesLabel(labels []string) bool {
FILE: pkg/gh/metrics.go
constant githubAPINamespace (line 16) | githubAPINamespace = "myshoes"
type instrumentedTransport (line 67) | type instrumentedTransport struct
method RoundTrip (line 81) | func (t *instrumentedTransport) RoundTrip(req *http.Request) (*http.Re...
function newInstrumentedTransport (line 71) | func newInstrumentedTransport(next http.RoundTripper) http.RoundTripper {
function classifyGitHubAPIError (line 121) | func classifyGitHubAPIError(err error) string {
FILE: pkg/gh/metrics_test.go
type stubTransport (line 13) | type stubTransport struct
method RoundTrip (line 18) | func (s *stubTransport) RoundTrip(req *http.Request) (*http.Response, ...
type timeoutErr (line 25) | type timeoutErr struct
method Error (line 27) | func (timeoutErr) Error() string { return "timeout" }
method Timeout (line 28) | func (timeoutErr) Timeout() bool { return true }
method Temporary (line 29) | func (timeoutErr) Temporary() bool { return true }
function TestInstrumentedTransportMetrics (line 31) | func TestInstrumentedTransportMetrics(t *testing.T) {
function TestInstrumentedTransportCacheHit (line 66) | func TestInstrumentedTransportCacheHit(t *testing.T) {
function TestInstrumentedTransportErrorMetrics (line 94) | func TestInstrumentedTransportErrorMetrics(t *testing.T) {
FILE: pkg/gh/ratelimit.go
function storeRateLimit (line 10) | func storeRateLimit(scope string, rateLimit github.Rate) {
function getRateLimitKey (line 20) | func getRateLimitKey(org, repo string) string {
function GetRateLimitRemain (line 29) | func GetRateLimitRemain() map[string]int {
function GetRateLimitLimit (line 54) | func GetRateLimitLimit() map[string]int {
FILE: pkg/gh/runner.go
function ExistGitHubRunner (line 14) | func ExistGitHubRunner(ctx context.Context, client *github.Client, owner...
function ExistGitHubRunnerWithRunner (line 24) | func ExistGitHubRunnerWithRunner(runners []*github.Runner, runnerName st...
function ListRunners (line 35) | func ListRunners(ctx context.Context, client *github.Client, owner, repo...
function getCacheKey (line 69) | func getCacheKey(owner, repo string) string {
function listRunners (line 73) | func listRunners(ctx context.Context, client *github.Client, owner, repo...
function GetLatestRunnerVersion (line 90) | func GetLatestRunnerVersion(ctx context.Context, scope string) (string, ...
function getRunnerVersion (line 128) | func getRunnerVersion(applications []*github.RunnerApplicationDownload) ...
function ConcatLabels (line 142) | func ConcatLabels(checkEventJSON string) (string, error) {
FILE: pkg/gh/scope.go
type Scope (line 6) | type Scope
method String (line 16) | func (s Scope) String() string {
constant Unknown (line 10) | Unknown Scope = iota
constant Repository (line 11) | Repository
constant Organization (line 12) | Organization
function DetectScope (line 28) | func DetectScope(scope string) Scope {
function DivideScope (line 41) | func DivideScope(scope string) (string, string) {
FILE: pkg/gh/token_registration.go
function GetRunnerRegistrationToken (line 17) | func GetRunnerRegistrationToken(ctx context.Context, installationID int6...
function generateRunnerRegisterToken (line 33) | func generateRunnerRegisterToken(ctx context.Context, installationID int...
function setRunnerRegisterTokenCache (line 58) | func setRunnerRegisterTokenCache(installationID int64, scope, token stri...
function getRunnerRegisterTokenFromCache (line 64) | func getRunnerRegisterTokenFromCache(installationID int64, scope string)...
function getCacheKeyRegistrationToken (line 76) | func getCacheKeyRegistrationToken(installationID int64, scope string) st...
FILE: pkg/gh/webhook.go
function parseEventJSON (line 12) | func parseEventJSON(in []byte) (interface{}, error) {
function ExtractRunsOnLabels (line 35) | func ExtractRunsOnLabels(in []byte) ([]string, error) {
FILE: pkg/gh/workflow_job.go
function listWorkflowJob (line 11) | func listWorkflowJob(ctx context.Context, client *github.Client, owner, ...
function ListWorkflowJobByRunID (line 20) | func ListWorkflowJobByRunID(ctx context.Context, client *github.Client, ...
function getWorkflowJobCacheKey (line 36) | func getWorkflowJobCacheKey(owner, repo string, runID int64) string {
FILE: pkg/gh/workflow_run.go
function listWorkflowRuns (line 12) | func listWorkflowRuns(ctx context.Context, client *github.Client, owner,...
function ListWorkflowRunsNewest (line 21) | func ListWorkflowRunsNewest(ctx context.Context, client *github.Client, ...
function getRunsCacheKey (line 57) | func getRunsCacheKey(owner, repo string) string {
FILE: pkg/logger/logger.go
function SetLogger (line 17) | func SetLogger(l *log.Logger) {
function Logf (line 27) | func Logf(isDebug bool, format string, v ...interface{}) {
FILE: pkg/metric/collector.go
constant namespace (line 15) | namespace = "myshoes"
type Collector (line 27) | type Collector struct
method Describe (line 45) | func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
method Collect (line 52) | func (c *Collector) Collect(ch chan<- prometheus.Metric) {
method scrape (line 60) | func (c *Collector) scrape(ctx context.Context, ch chan<- prometheus.M...
function NewCollector (line 35) | func NewCollector(ctx context.Context, ds datastore.Datastore) *Collector {
type Scraper (line 83) | type Scraper interface
function NewScrapers (line 90) | func NewScrapers() []Scraper {
type Metrics (line 99) | type Metrics struct
function NewMetrics (line 106) | func NewMetrics() Metrics {
FILE: pkg/metric/scrape_datastore.go
constant datastoreName (line 18) | datastoreName = "datastore"
type ScraperDatastore (line 63) | type ScraperDatastore struct
method Name (line 66) | func (ScraperDatastore) Name() string {
method Help (line 71) | func (ScraperDatastore) Help() string {
method Scrape (line 76) | func (ScraperDatastore) Scrape(ctx context.Context, ds datastore.Datas...
function scrapeJobs (line 90) | func scrapeJobs(ctx context.Context, ds datastore.Datastore, ch chan<- p...
function scrapeJobCounter (line 169) | func scrapeJobCounter(ctx context.Context, ds datastore.Datastore, ch ch...
function scrapeTargets (line 181) | func scrapeTargets(ctx context.Context, ds datastore.Datastore, ch chan<...
function scrapeRunners (line 212) | func scrapeRunners(ctx context.Context, ds datastore.Datastore, ch chan<...
FILE: pkg/metric/scrape_github.go
constant githubName (line 16) | githubName = "github"
type ScraperGitHub (line 45) | type ScraperGitHub struct
method Name (line 48) | func (ScraperGitHub) Name() string {
method Help (line 53) | func (ScraperGitHub) Help() string {
method Scrape (line 58) | func (s ScraperGitHub) Scrape(ctx context.Context, ds datastore.Datast...
function scrapePendingRuns (line 68) | func scrapePendingRuns(ctx context.Context, ds datastore.Datastore, ch c...
function scrapeInstallation (line 107) | func scrapeInstallation(ctx context.Context, ch chan<- prometheus.Metric...
FILE: pkg/metric/scrape_memory.go
constant memoryName (line 19) | memoryName = "memory"
type ScraperMemory (line 85) | type ScraperMemory struct
method Name (line 88) | func (ScraperMemory) Name() string {
method Help (line 93) | func (ScraperMemory) Help() string {
method Scrape (line 98) | func (ScraperMemory) Scrape(ctx context.Context, ds datastore.Datastor...
function scrapeStarterValues (line 113) | func scrapeStarterValues(ch chan<- prometheus.Metric) error {
function scrapeGitHubValues (line 162) | func scrapeGitHubValues(ch chan<- prometheus.Metric) error {
function scrapeDockerValues (line 182) | func scrapeDockerValues(ch chan<- prometheus.Metric) error {
FILE: pkg/runner/runner.go
type Manager (line 28) | type Manager struct
method Loop (line 42) | func (m *Manager) Loop(ctx context.Context) error {
function New (line 34) | func New(ds datastore.Datastore, runnerVersion string) *Manager {
type TemporaryMode (line 81) | type TemporaryMode
method StringFlag (line 91) | func (rtm TemporaryMode) StringFlag() string {
constant TemporaryUnknown (line 85) | TemporaryUnknown TemporaryMode = iota
constant TemporaryOnce (line 86) | TemporaryOnce
constant TemporaryEphemeral (line 87) | TemporaryEphemeral
function GetRunnerTemporaryMode (line 102) | func GetRunnerTemporaryMode(runnerVersion string) (string, TemporaryMode...
FILE: pkg/runner/runner_delete.go
method do (line 36) | func (m *Manager) do(ctx context.Context) error {
method removeRunners (line 55) | func (m *Manager) removeRunners(ctx context.Context, t datastore.Target)...
method removeRunner (line 151) | func (m *Manager) removeRunner(ctx context.Context, t datastore.Target, ...
function isRegisteredRunnerZeroInGitHub (line 181) | func isRegisteredRunnerZeroInGitHub(ctx context.Context, t datastore.Tar...
constant ErrDescriptionRunnerForQueueingIsNotFound (line 203) | ErrDescriptionRunnerForQueueingIsNotFound = "runner for queueing is not ...
function sanitizeGitHubRunner (line 213) | func sanitizeGitHubRunner(ghRunner github.Runner, dsRunner datastore.Run...
function sanitizeRunnerMustRunningTime (line 237) | func sanitizeRunnerMustRunningTime(runner datastore.Runner) error {
function sanitizeRunner (line 241) | func sanitizeRunner(runner datastore.Runner, needTime time.Duration) err...
method deleteRunnerWithGitHub (line 253) | func (m *Manager) deleteRunnerWithGitHub(ctx context.Context, githubClie...
method deleteRunner (line 277) | func (m *Manager) deleteRunner(ctx context.Context, runner datastore.Run...
FILE: pkg/runner/runner_delete_ephemeral.go
method removeRunnerModeEphemeral (line 16) | func (m *Manager) removeRunnerModeEphemeral(ctx context.Context, t datas...
FILE: pkg/runner/runner_delete_once.go
method removeRunnerModeOnce (line 16) | func (m *Manager) removeRunnerModeOnce(ctx context.Context, t datastore....
FILE: pkg/runner/token_update.go
method doTargetToken (line 13) | func (m *Manager) doTargetToken(ctx context.Context) error {
FILE: pkg/runner/util.go
function ToName (line 12) | func ToName(u string) string {
function ToUUID (line 17) | func ToUUID(name string) (uuid.UUID, error) {
function ToReason (line 23) | func ToReason(status string) datastore.RunnerStatus {
FILE: pkg/shoes/shoes.go
function GetClient (line 21) | func GetClient() (Client, func(), error) {
type Plugin (line 56) | type Plugin struct
method GRPCServer (line 63) | func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server)...
method GRPCClient (line 68) | func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBr...
type Client (line 73) | type Client interface
type GRPCClient (line 79) | type GRPCClient struct
method AddInstance (line 84) | func (c *GRPCClient) AddInstance(ctx context.Context, runnerName, setu...
method DeleteInstance (line 104) | func (c *GRPCClient) DeleteInstance(ctx context.Context, cloudID strin...
FILE: pkg/starter/error.go
type Error (line 5) | type Error struct
method Error (line 10) | func (e Error) Error() string {
method Unwrap (line 14) | func (e Error) Unwrap() error {
method Is (line 43) | func (e Error) Is(target error) bool {
type internalError (line 18) | type internalError
method String (line 24) | func (i internalError) String() string {
constant errorInvalidLabel (line 21) | errorInvalidLabel internalError = iota
function NewInvalidLabel (line 37) | func NewInvalidLabel(err error) error {
FILE: pkg/starter/metric.go
function incrementDeleteJobMap (line 16) | func incrementDeleteJobMap(j datastore.Job) error {
FILE: pkg/starter/safety/safety.go
type Safety (line 8) | type Safety interface
FILE: pkg/starter/safety/unlimited/unlimited.go
type Unlimited (line 7) | type Unlimited struct
method Check (line 10) | func (u Unlimited) Check(job *datastore.Job) (bool, error) {
FILE: pkg/starter/scripts.go
function getPatchedFiles (line 21) | func getPatchedFiles() (string, error) {
type templateCompressedScriptValue (line 25) | type templateCompressedScriptValue struct
method GetSetupScript (line 30) | func (s *Starter) GetSetupScript(ctx context.Context, targetScope, runne...
method getSetupRawScript (line 65) | func (s *Starter) getSetupRawScript(ctx context.Context, targetScope, ru...
function labelsToOneLine (line 124) | func labelsToOneLine(labels []string) string {
constant templateCompressedScript (line 132) | templateCompressedScript = `#!/bin/bash
type templateCreateLatestRunnerOnceValue (line 145) | type templateCreateLatestRunnerOnceValue struct
constant templateCreateLatestRunnerOnce (line 161) | templateCreateLatestRunnerOnce = `#!/bin/bash
FILE: pkg/starter/starter.go
type Starter (line 48) | type Starter struct
method Loop (line 66) | func (s *Starter) Loop(ctx context.Context) error {
method dispatcher (line 119) | func (s *Starter) dispatcher(ctx context.Context, ch chan datastore.Jo...
method run (line 134) | func (s *Starter) run(ctx context.Context, ch chan datastore.Job) error {
method ProcessJob (line 212) | func (s *Starter) ProcessJob(ctx context.Context, job datastore.Job) e...
method bung (line 336) | func (s *Starter) bung(ctx context.Context, job datastore.Job, target ...
method checkRegisteredRunner (line 409) | func (s *Starter) checkRegisteredRunner(ctx context.Context, runnerNam...
method reRunWorkflow (line 442) | func (s *Starter) reRunWorkflow(ctx context.Context) {
function New (line 56) | func New(ds datastore.Datastore, s safety.Safety, runnerVersion string, ...
function extractWorkflowIDs (line 193) | func extractWorkflowIDs(job datastore.Job) (runID int64, jobID int64, er...
function getTargetScope (line 381) | func getTargetScope(target datastore.Target, job datastore.Job) string {
function deleteInstance (line 388) | func deleteInstance(ctx context.Context, cloudID, checkEventJSON string)...
function reRunWorkflowByPendingRun (line 457) | func reRunWorkflowByPendingRun(ctx context.Context, ds datastore.Datasto...
function enqueueRescueRun (line 464) | func enqueueRescueRun(ctx context.Context, pendingRun datastore.PendingW...
function enqueueRescueJob (line 538) | func enqueueRescueJob(ctx context.Context, workflowJob *github.WorkflowJ...
FILE: pkg/web/config.go
type inputConfigDebug (line 11) | type inputConfigDebug struct
type inputConfigStrict (line 15) | type inputConfigStrict struct
function handleConfigDebug (line 19) | func handleConfigDebug(w http.ResponseWriter, r *http.Request) {
function handleConfigStrict (line 33) | func handleConfigStrict(w http.ResponseWriter, r *http.Request) {
FILE: pkg/web/http.go
function NewMux (line 19) | func NewMux(ds datastore.Datastore) *goji.Mux {
function Serve (line 82) | func Serve(ctx context.Context, ds datastore.Datastore) error {
function apacheLogging (line 107) | func apacheLogging(r *http.Request) {
FILE: pkg/web/http_test.go
function TestMain (line 10) | func TestMain(m *testing.M) {
FILE: pkg/web/metrics.go
function HandleMetrics (line 15) | func HandleMetrics(w http.ResponseWriter, r *http.Request, ds datastore....
FILE: pkg/web/target.go
type TargetCreateParam (line 23) | type TargetCreateParam struct
method ToDS (line 314) | func (t *TargetCreateParam) ToDS(appToken string, tokenExpired time.Ti...
type UserTarget (line 32) | type UserTarget struct
function sortUserTarget (line 44) | func sortUserTarget(uts []UserTarget) []UserTarget {
function handleTargetList (line 70) | func handleTargetList(w http.ResponseWriter, r *http.Request, ds datasto...
function handleTargetRead (line 93) | func handleTargetRead(w http.ResponseWriter, r *http.Request, ds datasto...
function sanitizeTarget (line 116) | func sanitizeTarget(t datastore.Target) UserTarget {
function handleTargetUpdate (line 132) | func handleTargetUpdate(w http.ResponseWriter, r *http.Request, ds datas...
function handleTargetDelete (line 187) | func handleTargetDelete(w http.ResponseWriter, r *http.Request, ds datas...
function parseReqTargetID (line 222) | func parseReqTargetID(r *http.Request) (uuid.UUID, error) {
type ErrorResponse (line 233) | type ErrorResponse struct
function outputErrorMsg (line 237) | func outputErrorMsg(w http.ResponseWriter, status int, msg string) {
function validateUpdateTarget (line 246) | func validateUpdateTarget(old, new datastore.Target) error {
function isValidTargetCreateParam (line 292) | func isValidTargetCreateParam(input TargetCreateParam) error {
function toNullString (line 300) | func toNullString(input *string) sql.NullString {
type getWillUpdateTargetVariableOld (line 327) | type getWillUpdateTargetVariableOld struct
type getWillUpdateTargetVariableNew (line 332) | type getWillUpdateTargetVariableNew struct
function getWillUpdateTargetVariable (line 337) | func getWillUpdateTargetVariable(oldParam getWillUpdateTargetVariableOld...
function getWillUpdateTargetVariableString (line 348) | func getWillUpdateTargetVariableString(old sql.NullString, new *string) ...
FILE: pkg/web/target_create.go
function handleTargetCreate (line 20) | func handleTargetCreate(w http.ResponseWriter, r *http.Request, ds datas...
function isValidScopeAndToken (line 126) | func isValidScopeAndToken(ctx context.Context, scope, githubPersonalToke...
function createNewTarget (line 146) | func createNewTarget(ctx context.Context, input datastore.Target, ds dat...
FILE: pkg/web/target_test.go
function parseResponse (line 27) | func parseResponse(resp *http.Response) ([]byte, int) {
function setStubFunctions (line 37) | func setStubFunctions() {
function Test_handleTargetCreate (line 67) | func Test_handleTargetCreate(t *testing.T) {
function Test_handleTargetCreate_alreadyRegistered (line 139) | func Test_handleTargetCreate_alreadyRegistered(t *testing.T) {
function Test_handleTargetCreate_recreated (line 169) | func Test_handleTargetCreate_recreated(t *testing.T) {
function Test_handleTargetCreate_recreated_update (line 228) | func Test_handleTargetCreate_recreated_update(t *testing.T) {
function Test_handleTargetList (line 289) | func Test_handleTargetList(t *testing.T) {
function Test_handleTargetRead (line 359) | func Test_handleTargetRead(t *testing.T) {
function Test_handleTargetUpdate (line 423) | func Test_handleTargetUpdate(t *testing.T) {
function Test_handleTargetUpdate_Error (line 534) | func Test_handleTargetUpdate_Error(t *testing.T) {
function Test_handleTargetDelete (line 586) | func Test_handleTargetDelete(t *testing.T) {
FILE: pkg/web/webhook.go
function HandleGitHubEvent (line 24) | func HandleGitHubEvent(w http.ResponseWriter, r *http.Request, ds datast...
function receivePingWebhook (line 108) | func receivePingWebhook(_ context.Context, event *github.PingEvent) error {
function receiveCheckRunWebhook (line 113) | func receiveCheckRunWebhook(ctx context.Context, event *github.CheckRunE...
function processCheckRun (line 143) | func processCheckRun(ctx context.Context, ds datastore.Datastore, repoNa...
function receiveWorkflowJobWebhook (line 188) | func receiveWorkflowJobWebhook(ctx context.Context, event *github.Workfl...
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (394K chars).
[
{
"path": ".github/workflows/build-docker-sha.yaml",
"chars": 1427,
"preview": "name: Build Docker image (sha)\non:\n push:\n branches:\n - \"**\"\n workflow_dispatch:\n\njobs:\n docker-build-sha:\n "
},
{
"path": ".github/workflows/release.yaml",
"chars": 1773,
"preview": "name: release\non:\n push:\n tags:\n - \"v[0-9]+.[0-9]+.[0-9]+\"\n\njobs:\n goreleaser:\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/test.yaml",
"chars": 1166,
"preview": "name: test\non:\n push:\n branches:\n - \"**\"\n pull_request:\n workflow_dispatch:\n\njobs:\n test:\n runs-on: ${{ m"
},
{
"path": ".gitignore",
"chars": 3284,
"preview": "\n# Created by https://www.toptal.com/developers/gitignore/api/macos,intellij,go\n# Edit at https://www.toptal.com/develop"
},
{
"path": ".goreleaser.yml",
"chars": 101,
"preview": "builds:\n - main: ./cmd/server/cmd.go\n goos:\n - linux\n goarch:\n - amd64\n - arm64"
},
{
"path": "Dockerfile",
"chars": 592,
"preview": "FROM golang:1.25 AS builder\n\nWORKDIR /go/src/github.com/whywaita/myshoes\n\nRUN go install google.golang.org/protobuf/cmd/"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2021 whywaita\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "Makefile",
"chars": 872,
"preview": ".PHONY: help\n.DEFAULT_GOAL := help\n\nCURRENT_REVISION = $(shell git rev-parse --short HEAD)\nBUILD_LDFLAGS = \"-X main.revi"
},
{
"path": "README.md",
"chars": 2591,
"preview": "# myshoes: Auto scaling self-hosted runner for GitHub Actions\n\n\n\n[![a"
},
{
"path": "api/myshoes/README.md",
"chars": 577,
"preview": "# myshoes-sdk-go\n\nThe Go SDK for myshoes\n\n## Usage\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/ht"
},
{
"path": "api/myshoes/client.go",
"chars": 1570,
"preview": "package myshoes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n)\n\n// Client is a cli"
},
{
"path": "api/myshoes/http.go",
"chars": 1130,
"preview": "package myshoes\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/web\"\n)\n\nfunc deco"
},
{
"path": "api/myshoes/target.go",
"chars": 2573,
"preview": "package myshoes\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/web"
},
{
"path": "api/proto/myshoes.proto",
"chars": 853,
"preview": "syntax = \"proto3\";\n\npackage whywaita.myshoes;\noption go_package = \"github.com/whywaita/myshoes/api/proto.go\";\n\nservice S"
},
{
"path": "api/proto.go/myshoes.pb.go",
"chars": 12719,
"preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc v5.29.3\n// so"
},
{
"path": "api/proto.go/myshoes_grpc.pb.go",
"chars": 5890,
"preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc "
},
{
"path": "cmd/server/cmd.go",
"chars": 2905,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes"
},
{
"path": "cmd/shoes-tester/main.go",
"chars": 9089,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strc"
},
{
"path": "docs/01_01_for_admin_setup.md",
"chars": 4606,
"preview": "# Setup myshoes daemon\n\n## Goal\n\n- Start myshoes daemon\n\n## Prepare\n\n- The network connectivity to myshoes server.\n - T"
},
{
"path": "docs/01_02_for_admin_tips.md",
"chars": 348,
"preview": "# Tips for myshoes admin\n\n## Job management hooks for self-hosted runners\n\nYou can use job management hooks for self-hos"
},
{
"path": "docs/02_01_for_user_setup.md",
"chars": 3479,
"preview": "# Setup (only once)\n\n## Goal\n\n- Start provision runner\n\n## Prepare\n\n- Get GitHub Apps's Public page from myshoes admin.\n"
},
{
"path": "docs/03_how-to-develop-shoes.md",
"chars": 3041,
"preview": "# How to develop shoes provider\n\n## TL;DR\n\n- implement to gRPC server\n - `shoes`, `health`, `stdio`\n- define resource"
},
{
"path": "docs/assets/myshoes.service",
"chars": 275,
"preview": "[Unit]\nDescription=myshoes is Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions\nAfter=net"
},
{
"path": "go.mod",
"chars": 3913,
"preview": "module github.com/whywaita/myshoes\n\ngo 1.25\n\nrequire (\n\tgithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0\n\tgithub.com/g"
},
{
"path": "go.sum",
"chars": 23427,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "internal/testutils/mysql.go",
"chars": 1940,
"preview": "package testutils\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"githu"
},
{
"path": "internal/testutils/testutils.go",
"chars": 1980,
"preview": "package testutils\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n"
},
{
"path": "internal/testutils/web.go",
"chars": 174,
"preview": "package testutils\n\n// GetTestURL return url of httptest.Server\nfunc GetTestURL() string {\n\tif testURL == \"\" {\n\t\tpanic(\"t"
},
{
"path": "internal/util/util.go",
"chars": 343,
"preview": "package util\n\nimport (\n\t\"math/rand/v2\"\n\t\"time\"\n)\n\n// CalcRetryTime is caliculate retry time by exponential backoff and j"
},
{
"path": "pkg/config/config.go",
"chars": 3245,
"preview": "package config\n\nimport (\n\t\"crypto/rsa\"\n\t\"strings\"\n)\n\n// Config is config value\nvar Config Conf\n\n// Conf is type of Confi"
},
{
"path": "pkg/config/init.go",
"chars": 8632,
"preview": "package config\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\""
},
{
"path": "pkg/datastore/github.go",
"chars": 5871,
"preview": "package datastore\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/whywaita/mysho"
},
{
"path": "pkg/datastore/interface.go",
"chars": 7072,
"preview": "package datastore\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\tuuid \"github.com"
},
{
"path": "pkg/datastore/memory/memory.go",
"chars": 5674,
"preview": "package memory\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github."
},
{
"path": "pkg/datastore/mysql/job.go",
"chars": 1470,
"preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github.com/whyw"
},
{
"path": "pkg/datastore/mysql/job_test.go",
"chars": 5359,
"preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/"
},
{
"path": "pkg/datastore/mysql/lock.go",
"chars": 1199,
"preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t"
},
{
"path": "pkg/datastore/mysql/mysql.go",
"chars": 1073,
"preview": "package mysql\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\n// MySQL is impl"
},
{
"path": "pkg/datastore/mysql/mysql_test.go",
"chars": 177,
"preview": "package mysql_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n)\n\nfunc TestMain(m *tes"
},
{
"path": "pkg/datastore/mysql/runner.go",
"chars": 5122,
"preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github."
},
{
"path": "pkg/datastore/mysql/runner_test.go",
"chars": 14592,
"preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/"
},
{
"path": "pkg/datastore/mysql/schema.sql",
"chars": 2993,
"preview": "CREATE TABLE `targets` (\n `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,\n `scope` VARCHAR(255) NOT NULL,\n `ghe_domain"
},
{
"path": "pkg/datastore/mysql/target.go",
"chars": 4040,
"preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github."
},
{
"path": "pkg/datastore/mysql/target_test.go",
"chars": 20735,
"preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/"
},
{
"path": "pkg/datastore/resource_type.go",
"chars": 4243,
"preview": "package datastore\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\tpb \"github.com/whywaita/myshoes/api/proto.g"
},
{
"path": "pkg/docker/ratelimit.go",
"chars": 3135,
"preview": "package docker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/golang-jw"
},
{
"path": "pkg/gh/github.go",
"chars": 7156,
"preview": "package gh\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bradleyfalzon/ghinstallation/v2"
},
{
"path": "pkg/gh/github_test.go",
"chars": 2442,
"preview": "package gh\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n)\n\nfunc TestDetectScope(t *testing.T) {"
},
{
"path": "pkg/gh/installation.go",
"chars": 3782,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/myshoes/"
},
{
"path": "pkg/gh/jwt.go",
"chars": 3274,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\n\t\"github.com/googl"
},
{
"path": "pkg/gh/jwt_test.go",
"chars": 2575,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\nfunc setStubFunctions()"
},
{
"path": "pkg/gh/label.go",
"chars": 443,
"preview": "package gh\n\nimport \"strings\"\n\n// IsRequestedMyshoesLabel checks if the job has appropriate labels for myshoes\nfunc IsReq"
},
{
"path": "pkg/gh/metrics.go",
"chars": 3352,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/m4ns0ur/httpcache\"\n\t\"github.co"
},
{
"path": "pkg/gh/metrics_test.go",
"chars": 3864,
"preview": "package gh\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/m4ns0ur/httpcache\"\n\t\"github.com/prometheus/clie"
},
{
"path": "pkg/gh/ratelimit.go",
"chars": 1280,
"preview": "package gh\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\nfunc storeRateLimit(scope string, rate"
},
{
"path": "pkg/gh/runner.go",
"chars": 4905,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywai"
},
{
"path": "pkg/gh/scope.go",
"chars": 883,
"preview": "package gh\n\nimport \"strings\"\n\n// Scope is scope for auto-scaling target\ntype Scope int\n\n// Scope values\nconst (\n\tUnknown"
},
{
"path": "pkg/gh/token_registration.go",
"chars": 2648,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/patrickmn/go-cache\"\n)\n\nvar (\n\tcacheRegistrationToken = cach"
},
{
"path": "pkg/gh/webhook.go",
"chars": 1297,
"preview": "package gh\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\n// parseEventJSON parse a jso"
},
{
"path": "pkg/gh/workflow_job.go",
"chars": 1316,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\nfunc listWorkflowJob(ctx co"
},
{
"path": "pkg/gh/workflow_run.go",
"chars": 1812,
"preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/myshoes/"
},
{
"path": "pkg/logger/logger.go",
"chars": 680,
"preview": "package logger\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n)\n\nvar (\n\tlogger = log.New(os.S"
},
{
"path": "pkg/metric/collector.go",
"chars": 3224,
"preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.c"
},
{
"path": "pkg/metric/scrape_datastore.go",
"chars": 6034,
"preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\""
},
{
"path": "pkg/metric/scrape_github.go",
"chars": 3644,
"preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\n\t\"gith"
},
{
"path": "pkg/metric/scrape_memory.go",
"chars": 6462,
"preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/prometheus/cl"
},
{
"path": "pkg/metric/webhook.go",
"chars": 1229,
"preview": "package metric\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometh"
},
{
"path": "pkg/runner/metrics.go",
"chars": 976,
"preview": "package runner\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometh"
},
{
"path": "pkg/runner/runner.go",
"chars": 2991,
"preview": "package runner\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/whywaita/myshoes/pkg"
},
{
"path": "pkg/runner/runner_delete.go",
"chars": 9780,
"preview": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/google/go-g"
},
{
"path": "pkg/runner/runner_delete_ephemeral.go",
"chars": 1974,
"preview": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/my"
},
{
"path": "pkg/runner/runner_delete_once.go",
"chars": 2038,
"preview": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/my"
},
{
"path": "pkg/runner/token_update.go",
"chars": 1689,
"preview": "package runner\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/m"
},
{
"path": "pkg/runner/util.go",
"chars": 722,
"preview": "package runner\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github.com/whywaita/myshoes/pkg/datastor"
},
{
"path": "pkg/shoes/shoes.go",
"chars": 3338,
"preview": "package shoes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/hashicorp/go-plugin\"\n\n\tpb \"github.com/whywaita/"
},
{
"path": "pkg/starter/README.md",
"chars": 51,
"preview": "## starter\n\nstarter is a dispatcher for running job"
},
{
"path": "pkg/starter/error.go",
"chars": 732,
"preview": "package starter\n\nimport \"errors\"\n\ntype Error struct {\n\tkind internalError\n\terr error\n}\n\nfunc (e Error) Error() string {"
},
{
"path": "pkg/starter/metric.go",
"chars": 606,
"preview": "package starter\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg"
},
{
"path": "pkg/starter/metrics.go",
"chars": 967,
"preview": "package starter\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/promet"
},
{
"path": "pkg/starter/safety/README.md",
"chars": 62,
"preview": "# safety\n\nsafety is interface of check to enable runner start."
},
{
"path": "pkg/starter/safety/safety.go",
"chars": 253,
"preview": "package safety\n\nimport (\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// Safety is interface for safety\ntype Safety i"
},
{
"path": "pkg/starter/safety/unlimited/unlimited.go",
"chars": 295,
"preview": "package unlimited\n\nimport \"github.com/whywaita/myshoes/pkg/datastore\"\n\n// Unlimited is implement of safety.\n// Unlimited"
},
{
"path": "pkg/starter/scripts/RunnerService.js",
"chars": 3151,
"preview": "#!/usr/bin/env node\n// Copyright (c) GitHub. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in"
},
{
"path": "pkg/starter/scripts.go",
"chars": 11802,
"preview": "package starter\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t_ \"embed\" // TODO:\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strings\"\n"
},
{
"path": "pkg/starter/starter.go",
"chars": 18947,
"preview": "package starter\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sync\"\n\t\"sync/atomic\""
},
{
"path": "pkg/web/config.go",
"chars": 1107,
"preview": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/mysh"
},
{
"path": "pkg/web/http.go",
"chars": 2921,
"preview": "package web\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n"
},
{
"path": "pkg/web/http_test.go",
"chars": 175,
"preview": "package web_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n)\n\nfunc TestMain(m *testi"
},
{
"path": "pkg/web/metrics.go",
"chars": 638,
"preview": "package web\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\n\t\"github.com/whywaita/myshoes/pkg/metri"
},
{
"path": "pkg/web/target.go",
"chars": 10208,
"preview": "package web\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/r3lab"
},
{
"path": "pkg/web/target_create.go",
"chars": 5712,
"preview": "package web\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\tuuid \"github.co"
},
{
"path": "pkg/web/target_test.go",
"chars": 19764,
"preview": "package web_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n"
},
{
"path": "pkg/web/webhook.go",
"chars": 7028,
"preview": "package web\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gi"
}
]
About this extraction
This page contains the full source code of the whywaita/myshoes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (350.2 KB), approximately 106.4k tokens, and a symbol index with 505 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.