From 499b06459811327d1ae9eb9f1acaddcdc4101082 Mon Sep 17 00:00:00 2001 From: Sergey Zhemoytel Date: Tue, 19 Nov 2024 17:42:08 +0300 Subject: [PATCH] Add Dockerfile, rewrite source --- Dockerfile | 20 ++++++++++ artifact_types/maven.go | 53 +++++++++++++++++++++++++ artifact_types/maven_test.go | 48 +++++++++++++++++++++++ artifact_types/rpm.go | 34 ++++++++++++++++ artifact_types/rpm_test.go | 52 +++++++++++++++++++++++++ config.yaml.template | 10 +++++ config/config.go | 34 ++++++++++++++++ config/config_test.go | 19 +++++++++ core/core.go | 66 +++++++++++++++++++++++++++++++ go.mod | 13 +++++++ go.sum | 17 ++++++++ logger/logger.go | 12 ++++++ main.go | 19 +++++++++ repository/gitea/gitea.go | 66 +++++++++++++++++++++++++++++++ repository/gitea/gitea_test.go | 14 +++++++ repository/nexus/nexus.go | 71 ++++++++++++++++++++++++++++++++++ repository/nexus/nexus_test.go | 14 +++++++ 17 files changed, 562 insertions(+) create mode 100644 Dockerfile create mode 100644 artifact_types/maven.go create mode 100644 artifact_types/maven_test.go create mode 100644 artifact_types/rpm.go create mode 100644 artifact_types/rpm_test.go create mode 100644 config.yaml.template create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 core/core.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logger/logger.go create mode 100644 main.go create mode 100644 repository/gitea/gitea.go create mode 100644 repository/gitea/gitea_test.go create mode 100644 repository/nexus/nexus.go create mode 100644 repository/nexus/nexus_test.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b76073 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM docker.io/golang:1.23-alpine AS builder + +COPY . /build + +RUN apk add --no-cache git && \ + ping -c5 tvoygit.ru && \ + export GOPROXY=direct && \ + cd /build && \ + go build -o ./app . + +FROM scratch + +COPY --from=builder /build/app /app + +CMD [/app] + + + + + diff --git a/artifact_types/maven.go b/artifact_types/maven.go new file mode 100644 index 0000000..6d160f1 --- /dev/null +++ b/artifact_types/maven.go @@ -0,0 +1,53 @@ +package artifact_types + +import ( + "fmt" +) + +type MavenArtifact struct { + GroupID string + ArtifactID string + Version string + Packaging string +} + +func (a MavenArtifact) String() string { + return fmt.Sprintf("%s:%s:%s:%s", a.GroupID, a.ArtifactID, a.Version, a.Packaging) +} + +func ParseMavenArtifact(artifact string) (*MavenArtifact, error) { + parts := splitArtifact(artifact, 4) + if len(parts) < 4 { + return nil, fmt.Errorf("invalid Maven artifact format: %s", artifact) + } + return &MavenArtifact{ + GroupID: parts[0], + ArtifactID: parts[1], + Version: parts[2], + Packaging: parts[3], + }, nil +} + +func splitArtifact(artifact string, n int) []string { + parts := make([]string, n) + for i := 0; i < n-1; i++ { + index := findNextColon(artifact) + if index == -1 { + break + } + parts[i] = artifact[:index] + artifact = artifact[index+1:] + } + parts[n-1] = artifact + return parts +} + +func findNextColon(s string) int { + for i, r := range s { + if r == ':' { + return i + } + } + return -1 +} + diff --git a/artifact_types/maven_test.go b/artifact_types/maven_test.go new file mode 100644 index 0000000..91c7de7 --- /dev/null +++ b/artifact_types/maven_test.go @@ -0,0 +1,48 @@ +package artifact_types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseMavenArtifact(t *testing.T) { + tests := []struct { + artifact string + expected *MavenArtifact + }{ + { + artifact: "com.example:my-artifact:1.0.0:jar", + expected: &MavenArtifact{ + GroupID: "com.example", + ArtifactID: "my-artifact", + Version: "1.0.0", + Packaging: "jar", + }, + }, + { + artifact: "org.apache.commons:commons-lang3:3.12.0:jar", + expected: &MavenArtifact{ + GroupID: "org.apache.commons", + ArtifactID: "commons-lang3", + Version: "3.12.0", + Packaging: "jar", + }, + }, + { + artifact: "invalid-artifact", + expected: nil, + }, + } + + for _, test := range tests { + actual, err := ParseMavenArtifact(test.artifact) + if test.expected == nil { + assert.Error(t, err, "Expected error for invalid artifact: %s", test.artifact) + } else { + assert.NoError(t, err, "Unexpected error for valid artifact: %s", test.artifact) + assert.Equal(t, test.expected, actual, "Mismatched parsed Maven artifact for: %s", test.artifact) + } + } +} + diff --git a/artifact_types/rpm.go b/artifact_types/rpm.go new file mode 100644 index 0000000..4667ef0 --- /dev/null +++ b/artifact_types/rpm.go @@ -0,0 +1,34 @@ +package artifact_types + +import ( + "fmt" + "regexp" + "strings" +) + +type RpmArtifact struct { + Name string + Version string + Release string + Arch string +} + +func (a RpmArtifact) String() string { + return fmt.Sprintf("%s-%s-%s.%s.rpm", a.Name, a.Version, a.Release, a.Arch) +} + +var rpmRegex = regexp.MustCompile(`^(.+)-([^-]+)-([^-]+)\.(.+)\.rpm$`) + +func ParseRpmArtifact(artifact string) (*RpmArtifact, error) { + matches := rpmRegex.FindStringSubmatch(artifact) + if matches == nil || len(matches) != 5 { + return nil, fmt.Errorf("invalid RPM artifact format: %s", artifact) + } + return &RpmArtifact{ + Name: matches[1], + Version: matches[2], + Release: matches[3], + Arch: matches[4], + }, nil +} + diff --git a/artifact_types/rpm_test.go b/artifact_types/rpm_test.go new file mode 100644 index 0000000..2ba81b9 --- /dev/null +++ b/artifact_types/rpm_test.go @@ -0,0 +1,52 @@ +package artifact_types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseRpmArtifact(t *testing.T) { + tests := []struct { + artifact string + expected *RpmArtifact + }{ + { + artifact: "example-1.0.0-1.x86_64.rpm", + expected: &RpmArtifact{ + Name: "example", + Version: "1.0.0", + Release: "1", + Arch: "x86_64", + }, + }, + { + artifact: "my-package-2.3.4-5.el7.noarch.rpm", + expected: &RpmArtifact{ + Name: "my-package", + Version: "2.3.4", + Release: "5.el7", + Arch: "noarch", + }, + }, + { + artifact: "invalid-artifact.rpm", + expected: nil, + }, + { + artifact: "invalid-artifact", + expected: nil, + }, + } + + for _, test := range tests { + actual, err := ParseRpmArtifact(test.artifact) + if test.expected == nil { + assert.Error(t, err, "Expected error for invalid artifact: %s", test.artifact) + } else { + assert.NoError(t, err, "Unexpected error for valid artifact: %s", test.artifact) + assert.Equal(t, test.expected, actual, "Mismatched parsed RPM artifact for: %s", test.artifact) + } + } +} + diff --git a/config.yaml.template b/config.yaml.template new file mode 100644 index 0000000..a2ae75b --- /dev/null +++ b/config.yaml.template @@ -0,0 +1,10 @@ +nexus: + url: "https://nexus.example.com" + username: "your-nexus-username" + password: "your-nexus-password" + +gitea: + url: "https://gitea.example.com" + token: "your-gitea-token" + + diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..08ad3e4 --- /dev/null +++ b/config/config.go @@ -0,0 +1,34 @@ +package config + +import ( + "log" + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Nexus struct { + URL string `yaml:"url"` + Username string `yaml:"username"` + Password string `yaml:"password"` + } `yaml:"nexus"` + Gitea struct { + URL string `yaml:"url"` + Token string `yaml:"token"` + } `yaml:"gitea"` +} + +func LoadConfig(filename string) (*Config, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var config Config + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, err + } + return &config, nil +} \ No newline at end of file diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..7d97a2b --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,19 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadConfig(t *testing.T) { + configFile := "config.yaml" + cfg, err := LoadConfig(configFile) + assert.NoError(t, err) + assert.NotNil(t, cfg) + assert.Equal(t, "https://nexus.example.com", cfg.Nexus.URL) + assert.Equal(t, "your-nexus-username", cfg.Nexus.Username) + assert.Equal(t, "your-nexus-password", cfg.Nexus.Password) + assert.Equal(t, "https://gitea.example.com", cfg.Gitea.URL) + assert.Equal(t, "your-gitea-token", cfg.Gitea.Token) +} \ No newline at end of file diff --git a/core/core.go b/core/core.go new file mode 100644 index 0000000..e29be64 --- /dev/null +++ b/core/core.go @@ -0,0 +1,66 @@ +package core + +import ( + "flag" + "fmt" + + "tvoygit.ru/djam/artmigrator/config" + "tvoygit.ru/djam/artmigrator/logger" + "tvoygit.ru/djam/artmigrator/repository/gitea" + "tvoygit.ru/djam/artmigrator/repository/nexus" +) + +type App struct { + Config *config.Config +} + +func NewApp(configFile string) (*App, error) { + cfg, err := config.LoadConfig(configFile) + if err != nil { + return nil, err + } + + flag.StringVar(&cfg.Nexus.URL, "nexus-url", cfg.Nexus.URL, "Nexus URL") + flag.StringVar(&cfg.Nexus.Username, "nexus-username", cfg.Nexus.Username, "Nexus Username") + flag.StringVar(&cfg.Nexus.Password, "nexus-password", cfg.Nexus.Password, "Nexus Password") + flag.StringVar(&cfg.Gitea.URL, "gitea-url", cfg.Gitea.URL, "Gitea URL") + flag.StringVar(&cfg.Gitea.Token, "gitea-token", cfg.Gitea.Token, "Gitea Token") + flag.Parse() + + return &App{Config: cfg}, nil +} + +func (a *App) Run() error { + nexusClient := nexus.NewClient(a.Config.Nexus.URL, a.Config.Nexus.Username, a.Config.Nexus.Password) + giteaClient := gitea.NewClient(a.Config.Gitea.URL, a.Config.Gitea.Token) + + repository := "your-repository-name" + artifacts, err := nexusClient.GetArtifacts(repository) + if err != nil { + return err + } + + for _, artifact := range artifacts { + exists, err := giteaClient.ArtifactExists("owner", "repo", artifact) + if err != nil { + return err + } + if exists { + logger.Logger.Printf("Artifact %s already exists in Gitea", artifact) + continue + } + + // Загрузка артефакта + data, err := nexusClient.GetArtifactData(repository, artifact) + if err != nil { + return err + } + + err = giteaClient.UploadArtifact("owner", "repo", artifact, data) + if err != nil { + return err + } + logger.Logger.Printf("Artifact %s uploaded to Gitea", artifact) + } + return nil +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0c7e528 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/yourusername/art_migrator + +go 1.19 + +require ( + github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6f0f12e --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..067be47 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,12 @@ +package logger + +import ( + "log" + "os" +) + +var Logger *log.Logger + +func init() { + Logger = log.New(os.Stdout, "", log.LstdFlags) +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..a06cde5 --- /dev/null +++ b/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + + "tvoygit.ru/djam/artmigrator/core" +) + +func main() { + app, err := core.NewApp("config.yaml") + if err != nil { + log.Fatalf("Failed to create app: %v", err) + } + + err = app.Run() + if err != nil { + log.Fatalf("Failed to run app: %v", err) + } +} \ No newline at end of file diff --git a/repository/gitea/gitea.go b/repository/gitea/gitea.go new file mode 100644 index 0000000..7e272a0 --- /dev/null +++ b/repository/gitea/gitea.go @@ -0,0 +1,66 @@ +package gitea + +import ( + "fmt" + "io/ioutil" + "net/http" + + "tvoygit.ru/djam/artmigrator/logger" +) + +type Client struct { + BaseURL string + HTTPClient *http.Client + Token string +} + +func NewClient(baseURL, token string) *Client { + return &Client{ + BaseURL: baseURL, + HTTPClient: &http.Client{}, + Token: token, + } +} + +func (c *Client) ArtifactExists(owner, repo, artifact string) (bool, error) { + url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents/%s", c.BaseURL, owner, repo, artifact) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + req.Header.Add("Authorization", fmt.Sprintf("token %s", c.Token)) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return false, nil + } + return true, nil +} + +func (c *Client) UploadArtifact(owner, repo, artifactPath string, data []byte) error { + url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents/%s", c.BaseURL, owner, repo, artifactPath) + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return err + } + req.Header.Add("Authorization", fmt.Sprintf("token %s", c.Token)) + + // Здесь нужно заполнить тело запроса данными о загружаемом артефакте + resp, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(resp.Body) + logger.Logger.Printf("Failed to upload artifact: %s", body) + return fmt.Errorf("failed to upload artifact") + } + return nil +} \ No newline at end of file diff --git a/repository/gitea/gitea_test.go b/repository/gitea/gitea_test.go new file mode 100644 index 0000000..650d35c --- /dev/null +++ b/repository/gitea/gitea_test.go @@ -0,0 +1,14 @@ +package gitea + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArtifactExists(t *testing.T) { + client := NewClient("https://gitea.example.com", "token") + exists, err := client.ArtifactExists("owner", "repo", "test-artifact") + assert.NoError(t, err) + assert.Equal(t, false, exists) +} \ No newline at end of file diff --git a/repository/nexus/nexus.go b/repository/nexus/nexus.go new file mode 100644 index 0000000..6246ea0 --- /dev/null +++ b/repository/nexus/nexus.go @@ -0,0 +1,71 @@ +package nexus + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + + "tvoygit.ru/djam/artmigrator/logger" +) + +type Client struct { + BaseURL string + HTTPClient *http.Client + Username string + Password string +} + +func NewClient(baseURL, username, password string) *Client { + return &Client{ + BaseURL: baseURL, + HTTPClient: &http.Client{}, + Username: username, + Password: password, + } +} + +func (c *Client) GetArtifacts(repository string) ([]string, error) { + url := fmt.Sprintf("%s/repository/%s/", c.BaseURL, repository) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.SetBasicAuth(c.Username, c.Password) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Здесь нужно парсить ответ и вытащить список артефактов + artifacts := strings.Split(string(body), "\n") + return artifacts, nil +} + +func (c *Client) GetArtifactData(repository, artifact string) ([]byte, error) { + url := fmt.Sprintf("%s/repository/%s/%s", c.BaseURL, repository, artifact) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.SetBasicAuth(c.Username, c.Password) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} \ No newline at end of file diff --git a/repository/nexus/nexus_test.go b/repository/nexus/nexus_test.go new file mode 100644 index 0000000..1e627b0 --- /dev/null +++ b/repository/nexus/nexus_test.go @@ -0,0 +1,14 @@ +package nexus + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetArtifacts(t *testing.T) { + client := NewClient("https://nexus.example.com", "user", "pass") + artifacts, err := client.GetArtifacts("test-repo") + assert.NoError(t, err) + assert.NotEmpty(t, artifacts) +} \ No newline at end of file