diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ab46cff --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,92 @@ +name: Release +# +on: + push: + #branches: + # - master + tags: + - 'v*.*.*' +# +jobs: + release: + runs-on: ubuntu-latest + # + steps: + - uses: actions/checkout@v3 + # + - name: Setup Go environment + uses: actions/setup-go@v3 + with: + go-version: '1.19' + - name: Install dependencies + run: go mod tidy + + - name: Build go + run: go build . + + - name: Get plugin metadata + id: metadata + run: | + sudo apt-get install jq + export NEXUS_ID=$(md5sum nexus-cli | sort | md5sum | cut -f1 -d ' ') + export NEXUS_VERSION=$(md5sum nexus-cli | sort | md5sum | cut -f1 -d ' ') + export NEXUS_PLUGIN_ARTIFACT=nexus-cli-${NEXUS_VERSION}.zip + export NEXUS_ARTIFACTS_CHECKSUM=${NEXUS_VERSION}.md5 + echo "::set-output name=plugin-id::nexus-cli" + echo "::set-output name=plugin-version::${NEXUS_VERSION}" + echo "::set-output name=archive::${NEXUS_PLUGIN_ARTIFACT}" + echo "::set-output name=archive-checksum::${NEXUS_ARTIFACTS_CHECKSUM}" + echo ::set-output name=github-tag::${GITHUB_REF#refs/*/} + # + - name: Read changelog + id: changelog + run: | + awk '/^## / {s++} s == 1 {print}' CHANGELOG.md > release_notes.md + echo "::set-output name=path::release_notes.md" + + - name: Package plugin + id: package-plugin + run: | + zip ${{ steps.metadata.outputs.archive }} nexus-cli -r + md5sum ${{ steps.metadata.outputs.archive }} > ${{ steps.metadata.outputs.archive-checksum }} + echo "::set-output name=checksum::$(cat ./${{ steps.metadata.outputs.archive-checksum }} | cut -d' ' -f1)" + + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body_path: ${{ steps.changelog.outputs.path }} + draft: true + # + - name: Add plugin to release + id: upload-plugin-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./${{ steps.metadata.outputs.archive }} + asset_name: ${{ steps.metadata.outputs.archive }} + asset_content_type: application/zip + # + - name: Add checksum to release + id: upload-checksum-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./${{ steps.metadata.outputs.archive-checksum }} + asset_name: ${{ steps.metadata.outputs.archive-checksum }} + asset_content_type: text/plain + # + - name: Publish to Release + run: | + echo A draft release has been created for your plugin. Please review and publish it. + echo + echo '{ "id": "${{ steps.metadata.outputs.plugin-id }}", "type": "${{ steps.metadata.outputs.plugin-type }}", "url": "https://github.com/${{ github.repository }}", "versions": [ { "version": "${{ steps.metadata.outputs.plugin-version }}", "commit": "${{ github.sha }}", "url": "https://github.com/${{ github.repository }}", "download": { "any": { "url": "https://github.com/${{ github.repository }}/releases/download/v${{ steps.metadata.outputs.plugin-version }}/${{ steps.metadata.outputs.archive }}", "md5": "${{ steps.package-plugin.outputs.checksum }}" } } } ] }' | jq +# \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bcf218d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +Nexus CLI \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af66c21 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/Allan-Nava/nexus-cli + +go 1.19 + +require ( + github.com/BurntSushi/toml v1.2.0 + github.com/mlabouardy/nexus-cli v0.0.0-20180823085010-e9ab90ee31be + github.com/urfave/cli v1.22.10 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/tidwall/gjson v1.14.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ad0d1b --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/mlabouardy/nexus-cli v0.0.0-20180823085010-e9ab90ee31be h1:5JIRQAv1vxCnmi/YThMfd7Wpr8Sz3gju9wUUZmh23fA= +github.com/mlabouardy/nexus-cli v0.0.0-20180823085010-e9ab90ee31be/go.mod h1:pmsbmSTdgWwFXXRGWoSScdYQRTEOiymG2DTVN1/FWMQ= +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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= +github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 2115b07..2d9f9fd 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,9 @@ import ( "fmt" "html/template" "os" + "sort" - "github.com/mlabouardy/nexus-cli/registry" + "github.com/Allan-Nava/nexus-cli/registry" "github.com/urfave/cli" ) @@ -21,12 +22,16 @@ func main() { app := cli.NewApp() app.Name = "Nexus CLI" app.Usage = "Manage Docker Private Registry on Nexus" - app.Version = "1.0.0-beta" + app.Version = "1.0.01" app.Authors = []cli.Author{ - cli.Author{ + { Name: "Mohamed Labouardy", Email: "mohamed@labouardy.com", }, + { + Name: "Allan Nava", + Email: "allan.nava@hiway.media", + }, } app.Commands = []cli.Command{ { @@ -179,20 +184,24 @@ func listTagsByImage(c *cli.Context) error { if imgName == "" { cli.ShowSubcommandHelp(c) } + var imageManifests []registry.ImageManifestV1 tags, err := r.ListTagsByImage(imgName) - - compareStringNumber := func(str1, str2 string) bool { - return extractNumberFromString(str1) < extractNumberFromString(str2) + for _, tag := range tags { + manifest, _ := r.ImageManifestV1(imgName, tag) + imageManifests = append(imageManifests, manifest) } - Compare(compareStringNumber).Sort(tags) + // + sort.Slice(imageManifests, func(i, j int) bool { + return imageManifests[i].Date.After(imageManifests[j].Date) + }) if err != nil { return cli.NewExitError(err.Error(), 1) } - for _, tag := range tags { - fmt.Println(tag) + for _, image := range imageManifests { + fmt.Println(image.Tag, " created: ", image.Created) } - fmt.Printf("There are %d images for %s\n", len(tags), imgName) + fmt.Printf("There are %d images for %s\n", len(imageManifests), imgName) return nil } @@ -237,20 +246,29 @@ func deleteImage(c *cli.Context) error { cli.ShowSubcommandHelp(c) } else { tags, err := r.ListTagsByImage(imgName) - compareStringNumber := func(str1, str2 string) bool { + /*compareStringNumber := func(str1, str2 string) bool { return extractNumberFromString(str1) < extractNumberFromString(str2) } - Compare(compareStringNumber).Sort(tags) + Compare(compareStringNumber).Sort(tags)*/ + var imageManifests []registry.ImageManifestV1 + for _, tag := range tags { + manifest, _ := r.ImageManifestV1(imgName, tag) + imageManifests = append(imageManifests, manifest) + } + // + sort.Slice(imageManifests, func(i, j int) bool { + return imageManifests[i].Date.Before(imageManifests[j].Date) + }) if err != nil { return cli.NewExitError(err.Error(), 1) } - if len(tags) >= keep { - for _, tag := range tags[:len(tags)-keep] { - fmt.Printf("%s:%s image will be deleted ...\n", imgName, tag) - r.DeleteImageByTag(imgName, tag) + if len(imageManifests) >= keep { + for _, tag := range imageManifests[:len(imageManifests)-keep] { + fmt.Printf("%s:%s image will be deleted date: %s...\n", imgName, tag.Tag, tag.Created) + r.DeleteImageByTag(imgName, tag.Tag) } } else { - fmt.Printf("Only %d images are available\n", len(tags)) + fmt.Printf("Only %d images are available\n", len(imageManifests)) } } } else { diff --git a/registry/registry.go b/registry/registry.go index 8f22558..287a71c 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -4,11 +4,20 @@ import ( "encoding/json" "errors" "fmt" - "github.com/BurntSushi/toml" + "io" + "log" "net/http" "os" + "time" + + "github.com/BurntSushi/toml" + "github.com/tidwall/gjson" ) + +var Start = time.Now() +var elapsed = time.Since(Start) +const ACCEPT_HEADER_V1 = "application/vnd.docker.distribution.manifest.v1+json" const ACCEPT_HEADER = "application/vnd.docker.distribution.manifest.v2+json" const CREDENTIALS_FILE = ".credentials" @@ -40,6 +49,15 @@ type LayerInfo struct { Digest string `json:"digest"` } +type ImageManifestV1 struct { + SchemaVersion int64 `json:"schemaVersion"` + Name string `json:"name"` + Tag string `json:"tag"` + Architecture string `json:"architecture"` + Created string + Date time.Time +} + func NewRegistry() (Registry, error) { r := Registry{} if _, err := os.Stat(CREDENTIALS_FILE); os.IsNotExist(err) { @@ -136,6 +154,91 @@ func (r Registry) ImageManifest(image string, tag string) (ImageManifest, error) } +func (r Registry) ImageManifestV1(image string, tag string) (ImageManifestV1, error) { + var tr = &http.Transport{ + MaxIdleConnsPerHost: 90, + } + var imageManifest ImageManifestV1 + var client = &http.Client{ + Transport: tr, + } +//elapsed = time.Since(Start) +// log.Printf("tag is %s", tag) +// log.Printf("begin get tag %s", elapsed) +// Start = time.Now() + url := fmt.Sprintf("%s/repository/%s/v2/%s/manifests/%s", r.Host, r.Repository, image, tag) + req, err := http.NewRequest("GET", url, nil) + req.Header = http.Header{ + "Accept": {"application/vnd.docker.distribution.manifest.v2+json"}, +} + if err != nil { + return imageManifest, err + } + req.SetBasicAuth(r.Username, r.Password) + req.Header.Add("Accept", ACCEPT_HEADER_V1) + + resp, err := client.Do(req) + if err != nil { + return imageManifest, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return imageManifest, errors.New(fmt.Sprintf("HTTP Code: %d", resp.StatusCode)) + } + b, err := io.ReadAll(resp.Body) + // b, err := ioutil.ReadAll(resp.Body) Go.1.15 and earlier + if err != nil { + log.Fatalln(err) + } + resp = nil + //json.NewDecoder(resp.Body).Decode(&imageManifest) + compatibilityString := gjson.GetBytes(b, `config`) + b = nil + digest := gjson.Get(compatibilityString.String(), `digest`) + +// log.Printf("digest is %s", digest) + + url = fmt.Sprintf("%s/repository/%s/v2/%s/blobs/%s", r.Host, r.Repository, image, digest) + req, err = http.NewRequest("GET", url, nil) + req.Header = http.Header{ + "Accept": {"application/vnd.docker.distribution.manifest.v2+json"}, +} + if err != nil { + return imageManifest, err + } + req.SetBasicAuth(r.Username, r.Password) + req.Header.Add("Accept", ACCEPT_HEADER_V1) + + resp, err = client.Do(req) + if err != nil { + return imageManifest, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return imageManifest, errors.New(fmt.Sprintf("HTTP Code: %d", resp.StatusCode)) + } + b, err = io.ReadAll(resp.Body) + // b, err := ioutil.ReadAll(resp.Body) Go.1.15 and earlier + if err != nil { + log.Fatalln(err) + } + resp = nil + //json.NewDecoder(resp.Body).Decode(&imageManifest) + created := gjson.Get(string(b),"created") + b = nil +if !created.Exists() { + return imageManifest, err +} + +// log.Printf("created is %s", created.String()) + imageManifest.Created = created.String() + imageManifest.Date = created.Time() + imageManifest.Tag = tag + imageManifest.Name = image + // + return imageManifest, nil +} + func (r Registry) DeleteImageByTag(image string, tag string) error { sha, err := r.getImageSHA(image, tag) if err != nil { @@ -188,4 +291,4 @@ func (r Registry) getImageSHA(image string, tag string) (string, error) { } return resp.Header.Get("docker-content-digest"), nil -} +} \ No newline at end of file