forked from forgejo/forgejo
Add Chef package registry (#22554)
This PR implements a [Chef registry](https://chef.io/) to manage cookbooks. This package type was a bit complicated because Chef uses RSA signed requests as authentication with the registry.   Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
ff18d17442
commit
d987ac6bf1
31 changed files with 1737 additions and 20 deletions
134
modules/packages/chef/metadata.go
Normal file
134
modules/packages/chef/metadata.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package chef
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyBits = 4096
|
||||
SettingPublicPem = "chef.public_pem"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.json file is missing")
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||
|
||||
namePattern = regexp.MustCompile(`\A\S+\z`)
|
||||
versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`)
|
||||
)
|
||||
|
||||
// Package represents a Chef package
|
||||
type Package struct {
|
||||
Name string
|
||||
Version string
|
||||
Metadata *Metadata
|
||||
}
|
||||
|
||||
// Metadata represents the metadata of a Chef package
|
||||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
LongDescription string `json:"long_description,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
type chefMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
LongDescription string `json:"long_description"`
|
||||
Maintainer string `json:"maintainer"`
|
||||
MaintainerEmail string `json:"maintainer_email"`
|
||||
License string `json:"license"`
|
||||
Platforms map[string]string `json:"platforms"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
Providing map[string]string `json:"providing"`
|
||||
Recipes map[string]string `json:"recipes"`
|
||||
Version string `json:"version"`
|
||||
SourceURL string `json:"source_url"`
|
||||
IssuesURL string `json:"issues_url"`
|
||||
Privacy bool `json:"privacy"`
|
||||
ChefVersions [][]string `json:"chef_versions"`
|
||||
Gems [][]string `json:"gems"`
|
||||
EagerLoadLibraries bool `json:"eager_load_libraries"`
|
||||
}
|
||||
|
||||
// ParsePackage parses the Chef package file
|
||||
func ParsePackage(r io.Reader) (*Package, error) {
|
||||
gzr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hd.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Count(hd.Name, "/") != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if hd.FileInfo().Name() == "metadata.json" {
|
||||
return ParseChefMetadata(tr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMissingMetadataFile
|
||||
}
|
||||
|
||||
// ParseChefMetadata parses a metadata.json file to retrieve the metadata of a Chef package
|
||||
func ParseChefMetadata(r io.Reader) (*Package, error) {
|
||||
var cm chefMetadata
|
||||
if err := json.NewDecoder(r).Decode(&cm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !namePattern.MatchString(cm.Name) {
|
||||
return nil, ErrInvalidName
|
||||
}
|
||||
|
||||
if !versionPattern.MatchString(cm.Version) {
|
||||
return nil, ErrInvalidVersion
|
||||
}
|
||||
|
||||
if !validation.IsValidURL(cm.SourceURL) {
|
||||
cm.SourceURL = ""
|
||||
}
|
||||
|
||||
return &Package{
|
||||
Name: cm.Name,
|
||||
Version: cm.Version,
|
||||
Metadata: &Metadata{
|
||||
Description: cm.Description,
|
||||
LongDescription: cm.LongDescription,
|
||||
Author: cm.Maintainer,
|
||||
License: cm.License,
|
||||
RepositoryURL: cm.SourceURL,
|
||||
Dependencies: cm.Dependencies,
|
||||
},
|
||||
}, nil
|
||||
}
|
92
modules/packages/chef/metadata_test.go
Normal file
92
modules/packages/chef/metadata_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package chef
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
packageName = "gitea"
|
||||
packageVersion = "1.0.1"
|
||||
packageAuthor = "KN4CK3R"
|
||||
packageDescription = "Package Description"
|
||||
packageRepositoryURL = "https://gitea.io/gitea/gitea"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
t.Run("MissingMetadataFile", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
tw := tar.NewWriter(zw)
|
||||
tw.Close()
|
||||
zw.Close()
|
||||
|
||||
p, err := ParsePackage(&buf)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrMissingMetadataFile)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
tw := tar.NewWriter(zw)
|
||||
|
||||
content := `{"name":"` + packageName + `","version":"` + packageVersion + `"}`
|
||||
|
||||
hdr := &tar.Header{
|
||||
Name: packageName + "/metadata.json",
|
||||
Mode: 0o600,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
tw.WriteHeader(hdr)
|
||||
tw.Write([]byte(content))
|
||||
|
||||
tw.Close()
|
||||
zw.Close()
|
||||
|
||||
p, err := ParsePackage(&buf)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
assert.Equal(t, packageName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.NotNil(t, p.Metadata)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseChefMetadata(t *testing.T) {
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
for _, name := range []string{" test", "test "} {
|
||||
p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + name + `","version":"1.0.0"}`))
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidVersion", func(t *testing.T) {
|
||||
for _, version := range []string{"1", "1.2.3.4", "1.0.0 "} {
|
||||
p, err := ParseChefMetadata(strings.NewReader(`{"name":"test","version":"` + version + `"}`))
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + packageName + `","version":"` + packageVersion + `","description":"` + packageDescription + `","maintainer":"` + packageAuthor + `","source_url":"` + packageRepositoryURL + `"}`))
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, packageName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.Equal(t, packageDescription, p.Metadata.Description)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author)
|
||||
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue