feat: initial copy of v0.1.0
All checks were successful
Publish / Check GoReleaser config (push) Successful in 5s
Publish / Publish provider (push) Successful in 16m14s

This commit is contained in:
Marcel S. Henselin 2026-03-13 09:03:49 +01:00
parent 4cc801a7f3
commit 7d4cbb6b08
538 changed files with 63361 additions and 55213 deletions

View file

@ -0,0 +1,137 @@
package publish
import (
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"path"
"strings"
)
type Architecture struct {
Protocols []string `json:"protocols"`
OS string `json:"os"`
Arch string `json:"arch"`
FileName string `json:"filename"`
DownloadUrl string `json:"download_url"`
ShaSumsUrl string `json:"shasums_url"`
ShaSumsSignatureUrl string `json:"shasums_signature_url"`
ShaSum string `json:"shasum"`
SigningKeys SigningKey `json:"signing_keys"`
}
type SigningKey struct {
GpgPublicKeys []GpgPublicKey `json:"gpg_public_keys"`
}
type GpgPublicKey struct {
KeyId string `json:"key_id"`
AsciiArmor string `json:"ascii_armor"`
TrustSignature string `json:"trust_signature"`
Source string `json:"source"`
SourceUrl string `json:"source_url"`
}
func (p *Provider) CreateArchitectureFiles() error {
log.Println("* Creating architecture files in target directories")
prefix := path.Join("v1", "providers", p.Namespace, p.Provider, p.Version)
pathPrefix := path.Join("release", prefix)
urlPrefix, err := url.JoinPath("https://", p.Domain, prefix)
if err != nil {
return fmt.Errorf("error creating base url: %w", err)
}
downloadUrlPrefix, err := url.JoinPath(urlPrefix, "download")
if err != nil {
return fmt.Errorf("error crearting download url: %w", err)
}
downloadPathPrefix := path.Join(pathPrefix, "download")
shasumsUrl, err := url.JoinPath(urlPrefix, fmt.Sprintf("%s_%s_SHA256SUMS", p.RepoName, p.Version))
if err != nil {
return fmt.Errorf("error creating shasums url: %w", err)
}
shasumsSigUrl := shasumsUrl + ".sig"
gpgAsciiPub, err := p.ReadGpgFile()
if err != nil {
return err
}
shaSums, err := p.GetShaSums()
if err != nil {
return err
}
for _, sum := range shaSums {
downloadUrl, err := url.JoinPath(downloadUrlPrefix, sum.Path)
if err != nil {
return fmt.Errorf("error creating url: %w", err)
}
// get os and arch from filename
removeFileExtension := strings.Split(sum.Path, ".zip")
fileNameSplit := strings.Split(removeFileExtension[0], "_")
// Get build target and architecture from the zip file name
target := fileNameSplit[2]
arch := fileNameSplit[3]
// build filepath
archFileName := path.Join(downloadPathPrefix, target, arch)
a := Architecture{
Protocols: []string{"5.1", "6.0"},
OS: target,
Arch: arch,
FileName: sum.Path,
DownloadUrl: downloadUrl,
ShaSumsUrl: shasumsUrl,
ShaSumsSignatureUrl: shasumsSigUrl,
ShaSum: sum.Sum,
SigningKeys: SigningKey{},
}
a.SigningKeys = SigningKey{
GpgPublicKeys: []GpgPublicKey{
{
KeyId: p.GpgFingerprint,
AsciiArmor: gpgAsciiPub,
TrustSignature: "",
Source: "",
SourceUrl: "",
},
},
}
log.Printf(" - Arch file: %s", archFileName)
err = WriteArchitectureFile(archFileName, a)
if err != nil {
return err
}
}
return nil
}
func WriteArchitectureFile(filePath string, arch Architecture) error {
jsonString, err := json.Marshal(arch)
if err != nil {
return fmt.Errorf("error encoding data: %w", err)
}
//nolint:gosec // this file is not sensitive, so we can use os.ModePerm
err = os.WriteFile(
filePath,
jsonString,
os.ModePerm,
)
if err != nil {
return fmt.Errorf("error writing data: %w", err)
}
return nil
}

View file

@ -0,0 +1,14 @@
package publish
import (
"fmt"
"strings"
)
func (p *Provider) ReadGpgFile() (string, error) {
gpgFile, err := ReadFile(p.GpgPubKeyFile)
if err != nil {
return "", fmt.Errorf("error reading '%s' file: %w", p.GpgPubKeyFile, err)
}
return strings.Join(gpgFile, "\n"), nil
}

View file

@ -0,0 +1,297 @@
package publish
import (
"bufio"
"errors"
"fmt"
"io"
"io/fs"
"log"
"log/slog"
"os"
"os/exec"
"path"
"strings"
)
type Provider struct {
RootPath string
Namespace string
Provider string
DistPath string
RepoName string
Version string
GpgFingerprint string
GpgPubKeyFile string
Domain string
}
func (p *Provider) GetRoot() error {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
out, err := cmd.Output()
if err != nil {
return err
}
lines := strings.Split(string(out), "\n")
p.RootPath = lines[0]
return nil
}
func (p *Provider) CreateV1Dir() error {
// Path to semantic version dir
versionPath := p.providerDirs()
// Files to create under v1/providers/[namespace]/[provider_name]
err := p.createVersionsFile()
if err != nil {
return fmt.Errorf("[CreateV1Dir] - create versions file:%w", err)
} // Creates version file one above download, which is why downloadPath isn't used
// Files/Directories to create under v1/providers/[namespace]/[provider_name]/[version]
err = p.copyShaFiles(versionPath)
if err != nil {
return fmt.Errorf("[CreateV1Dir] - copy sha files: %w", err)
}
log.Printf("* Creating download/ in %s directory", versionPath)
downloadsPath := path.Join(versionPath, "download")
err = CreateDir(downloadsPath)
if err != nil {
return err
}
// Create darwin, freebsd, linux, windows dirs
for _, v := range [4]string{"darwin", "freebsd", "linux", "windows"} {
err = CreateDir(path.Join(downloadsPath, v))
if err != nil {
return fmt.Errorf("error creating dir '%s': %w", path.Join(downloadsPath, v), err)
}
}
// Copy all zips
err = p.copyBuildZips(downloadsPath)
if err != nil {
return err
}
// Create all individual files for build targets and each architecture for the build targets
err = p.CreateArchitectureFiles()
if err != nil {
return err
}
return nil
}
func (p *Provider) copyBuildZips(destPath string) error {
log.Println("* Copying build zips")
shaSums, err := p.GetShaSums()
if err != nil {
return err
}
// Loop through and copy each
for _, sum := range shaSums {
zipSrcPath := path.Join(p.DistPath, sum.Path)
zipDestPath := path.Join(destPath, sum.Path)
log.Printf(" - Zip Source: %s", zipSrcPath)
log.Printf(" - Zip Dest: %s", zipDestPath)
// Copy the zip
_, err = CopyFile(zipSrcPath, zipDestPath)
if err != nil {
return fmt.Errorf("error copying file '%s': %w", zipSrcPath, err)
}
}
return nil
}
func (p *Provider) copyShaFiles(destPath string) error {
log.Printf("* Copying SHA files in %s directory", p.DistPath)
// Copy files from srcPath
shaSum := p.RepoName + "_" + p.Version + "_SHA256SUMS"
shaSumPath := path.Join(p.DistPath, shaSum)
// _SHA256SUMS file
_, err := CopyFile(shaSumPath, path.Join(destPath, shaSum))
if err != nil {
return err
}
// _SHA256SUMS.sig file
_, err = CopyFile(shaSumPath+".sig", path.Join(destPath, shaSum+".sig"))
if err != nil {
return err
}
return nil
}
func (p *Provider) createVersionsFile() error {
log.Println("* Writing to release/v1/providers/[namespace]/[repo]/versions file")
versionPath := path.Join("release", "v1", "providers", p.Namespace, p.Provider, "versions")
shasums, err := p.GetShaSums()
if err != nil {
return fmt.Errorf("error getting sha sums: %w", err)
}
// Build the versions file...
version := Version{
Version: p.Version,
Protocols: []string{"5.1", "6.1"},
Platforms: nil,
}
for _, sum := range shasums {
// get os and arch from filename
removeFileExtension := strings.Split(sum.Path, ".zip")
if len(removeFileExtension) < 1 {
log.Fatalf("error: %s does not have .zip extension", sum.Path)
}
fileNameSplit := strings.Split(removeFileExtension[0], "_")
if len(fileNameSplit) < 4 {
log.Fatalf("filename does not match our regex: %s", removeFileExtension[0])
}
// Get build target and architecture from the zip file name
target := fileNameSplit[2]
arch := fileNameSplit[3]
version.Platforms = append(
version.Platforms, Platform{
OS: target,
Arch: arch,
},
)
}
data := Data{}
downloadPath := path.Join(p.Domain, "v1", "providers", p.Namespace, p.Provider, "versions")
err = data.LoadFromUrl(fmt.Sprintf("https://%s", downloadPath))
if err != nil {
slog.Warn("error getting existing versions file, start with empty")
// TODO: create flag for first use or make it more robust
// return fmt.Errorf("error getting existing versions file: %w", err)
}
err = data.AddVersion(version)
if err != nil {
return fmt.Errorf("error appending version: %w", err)
}
err = data.WriteToFile(versionPath)
if err != nil {
return fmt.Errorf("error saving file '%s':%w", versionPath, err)
}
return nil
}
func (p *Provider) providerDirs() string {
log.Println("* Creating release/v1/providers/[namespace]/[provider]/[version] directories")
target := path.Join("release", "v1", "providers", p.Namespace, p.Provider, p.Version)
err := CreateDir(target)
if err != nil {
return ""
}
return target
}
func (p *Provider) CreateWellKnown() error {
log.Println("* Creating .well-known directory")
pathString := path.Join(p.RootPath, "release", ".well-known")
//nolint:gosec // this file is not sensitive, so we can use ModePerm
err := os.MkdirAll(pathString, os.ModePerm)
if err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("error creating '%s' dir: %w", pathString, err)
}
log.Println(" - Writing to .well-known/terraform.json file")
//nolint:gosec // this file is not sensitive, so we can use 0644
err = os.WriteFile(
fmt.Sprintf("%s/terraform.json", pathString),
[]byte(`{"providers.v1": "/v1/providers/"}`),
0o644,
)
if err != nil {
return err
}
return nil
}
func CreateDir(pathValue string) error {
log.Printf("* Creating %s directory", pathValue)
//nolint:gosec // this file is not sensitive, so we can use ModePerm
err := os.MkdirAll(pathValue, os.ModePerm)
if errors.Is(err, fs.ErrExist) {
return nil
}
return err
}
func ReadFile(filePath string) ([]string, error) {
rFile, err := os.Open(filePath)
if err != nil {
return nil, err
}
fileScanner := bufio.NewScanner(rFile)
fileScanner.Split(bufio.ScanLines)
var fileLines []string
for fileScanner.Scan() {
fileLines = append(fileLines, fileScanner.Text())
}
err = rFile.Close()
if err != nil {
return nil, err
}
return fileLines, nil
}
func CopyFile(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return 0, err
}
if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return 0, err
}
defer func(source *os.File) {
err := source.Close()
if err != nil {
slog.Error("error closing source file", slog.Any("err", err))
}
}(source)
destination, err := os.Create(dst)
if err != nil {
return 0, err
}
defer func(destination *os.File) {
err := destination.Close()
if err != nil {
slog.Error("error closing destination file", slog.Any("err", err))
}
}(destination)
nBytes, err := io.Copy(destination, source)
return nBytes, err
}

View file

@ -0,0 +1,39 @@
package publish
import (
"log/slog"
"path"
"regexp"
)
func (p *Provider) GetShaSums() (ShaSums, error) {
return GetShaSumContents(p.DistPath, p.RepoName, p.Version)
}
type ShaSums []ShaSum
type ShaSum struct {
Sum string
Path string
}
func GetShaSumContents(distPath, repoName, version string) (ShaSums, error) {
shaSumFileName := repoName + "_" + version + "_SHA256SUMS"
shaSumPath := path.Join(distPath, shaSumFileName)
shaSumLine, err := ReadFile(shaSumPath)
if err != nil {
return nil, err
}
regEx := regexp.MustCompile(`([0-9a-fA-F]+)\s+(.+)`)
shaSums := ShaSums{}
for _, line := range shaSumLine {
matches := regEx.FindAllStringSubmatch(line, -1)
if len(matches) < 1 {
slog.Warn("unable to parse SHA sum line", "line", line)
continue
}
shaSums = append(shaSums, ShaSum{Sum: matches[0][1], Path: matches[0][2]})
}
return shaSums, nil
}

View file

@ -0,0 +1,38 @@
{
log {
level debug
}
filesystem tf s3 {
bucket "terraform-provider-privatepreview"
region eu01
endpoint https://object.storage.eu01.onstackit.cloud
use_path_style
}
}
tfregistry.sysops.stackit.rocks {
encode zstd gzip
handle_path /docs/* {
root /srv/www
templates
@md {
file {path}
path *.md
}
rewrite @md /markdown.html
file_server {
browse
}
}
file_server {
fs tf
browse
}
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>Forwarding | Weiterleitung</title>
<meta http-equiv="refresh" content="0; URL=index.md">
</head>
<body>
<a href="index.md">Falls Sie nicht automatisch weitergeleitet werden, klicken Sie bitte hier.</a><br />
Sie gelangen dann auf unsere Hauptseite
</body>
</html>

View file

@ -0,0 +1,34 @@
---
page_title: STACKIT provider PrivatePreview
description: none
---
# provider
[Provider](docs/index.md)
## PostGreSQL alpha
### data sources
- [Flavor](docs/data-sources/postgresflexalpha_flavor.md)
- [Database](docs/data-sources/postgresflexalpha_database.md)
- [Instance](docs/data-sources/postgresflexalpha_instance.md)
- [Flavors](docs/data-sources/postgresflexalpha_flavors.md)
- [User](docs/data-sources/postgresflexalpha_user.md)
### resources
- [Database](docs/resources/postgresflexalpha_database.md)
- [Instance](docs/resources/postgresflexalpha_instance.md)
- [User](docs/resources/postgresflexalpha_user.md)
## SQL Server alpha
### data sources
- [Database](docs/data-sources/sqlserverflexalpha_database.md)
- [Version](docs/data-sources/sqlserverflexalpha_version.md)
- [User](docs/data-sources/sqlserverflexalpha_user.md)
- [Flavor](docs/data-sources/sqlserverflexalpha_flavor.md)
- [Instance](docs/data-sources/sqlserverflexalpha_instance.md)
### resources
- [Database](docs/resources/sqlserverflexalpha_database.md)
- [User](docs/resources/sqlserverflexalpha_user.md)
- [Instance](docs/resources/sqlserverflexalpha_instance.md)

View file

@ -0,0 +1,79 @@
<!DOCTYPE html>
{{ $mdFile := .OriginalReq.URL.Path | trimPrefix "/docs" }}
{{ $md := (include $mdFile | splitFrontMatter) }}
<html lang="en">
<head>
<title>{{$md.Meta.page_title}}</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="/docs/terraform-registry.css">
</head>
<body>
<h1>{{$md.Meta.page_title}}</h1>
<div class="provider-view">
<div class="provider-nav">
<nav class="bread-crumbs is-light" aria-label="Provider">
<div class="container is-widescreen">
<div class="level">
<ul class="provider-nav-breadcrumbs bread-crumbs-list">
<li class="bread-crumbs-item">
<a id="ember20" class="ember-view bread-crumbs-link" href="/">
Main
</a>
</li>
</ul>
</div>
</div>
</nav>
<nav class="block-border section-navbar section-header" aria-label="Provider details">
<div class="container">
<div class="columns is-vcentered">
<div class="column is-4">
<div class="provider-nav-info-header">
<div class="provider-overview-logo">
<span class="provider-logo">
<img class="github-image" src="https://avatars3.githubusercontent.com/stackitcloud" alt="stackitcloud">
</span>
</div>
<div class="provider-nav-info-origin">
<h1>PRIVATE PREVIEW</h1>
</div>
</div>
</div>
<div class="column is-8">
<ul class="nav-tabs-list nav-tabs tabs">
<li class="nav-tabs-item">
<a id="ember30" class="ember-view navbar-item" href="/">
Overview
</a>
</li>
</ul>
<div class="provider-nav-provision-wrapper">
<!----> </div>
</div>
</div>
</div>
</nav>
</div>
<div class="section block-border block-white section-content">
<div class="container">
<div class="columns columns-provider-docs">
<div class="column is-3 column-provider-docs-menu"></div>
<article id="provider-docs-content" class="column is-6 provider-docs-content">
<div class="markdown">
<div class="highlighted-code-wrapper">
{{markdown $md.Body}}
</div>
</div>
</article>
<div class="column is-3 column-provider-docs-menu"></div>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,169 @@
package publish
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
)
type Version struct {
Version string `json:"version"`
Protocols []string `json:"protocols"`
Platforms []Platform `json:"platforms"`
}
type Platform struct {
OS string `json:"os" yaml:"os"`
Arch string `json:"arch" yaml:"arch"`
}
type Data struct {
Id string `json:"id,omitempty"`
Versions []Version `json:"versions"`
}
func (d *Data) WriteToFile(filePath string) error {
// TODO: make it variable
d.Id = "tfregistry.sysops.stackit.rocks/mhenselin/stackitprivatepreview"
jsonString, err := json.Marshal(d)
if err != nil {
return fmt.Errorf("error encoding data: %w", err)
}
//nolint:gosec // this file is not sensitive, so we can use os.ModePerm
err = os.WriteFile(
filePath,
jsonString,
os.ModePerm,
)
if err != nil {
return fmt.Errorf("error writing data: %w", err)
}
return nil
}
func (d *Data) AddVersion(v Version) error {
var newVersions []Version
for _, ver := range d.Versions {
if ver.Version != v.Version {
newVersions = append(newVersions, ver)
}
}
newVersions = append(newVersions, v)
d.Versions = newVersions
return nil
}
func (d *Data) Validate() error {
for _, v := range d.Versions {
err := v.Validate()
if err != nil {
return err
}
}
return nil
}
func (d *Data) LoadFromFile(filePath string) error {
plan, err := os.ReadFile(filePath)
if err != nil {
return err
}
err = json.Unmarshal(plan, &d)
if err != nil {
return err
}
return nil
}
func (d *Data) LoadFromUrl(uri string) error {
u, err := url.ParseRequestURI(uri)
if err != nil {
return err
}
file, err := os.CreateTemp("", "versions.*.json")
if err != nil {
return err
}
defer func(name string) {
//nolint:gosec // The file path is generated by os.CreateTemp and is not user-controllable
err := os.Remove(name)
if err != nil {
slog.Error("failed to remove temporary file", slog.Any("err", err))
}
}(file.Name()) // Clean up
err = DownloadFile(
u.String(),
file.Name(),
)
if err != nil {
return err
}
return d.LoadFromFile(file.Name())
}
func (v *Version) Validate() error {
slog.Warn("validation needs to be implemented")
return nil
}
func (v *Version) AddPlatform(p Platform) error {
if p.OS == "" || p.Arch == "" {
return fmt.Errorf("OS and Arch MUST NOT be empty")
}
v.Platforms = append(v.Platforms, p)
return nil
}
func (v *Version) AddProtocol(p string) error {
if p == "" {
return fmt.Errorf("protocol MUST NOT be empty")
}
v.Protocols = append(v.Protocols, p)
return nil
}
// DownloadFile will download a url and store it in local filepath.
// It writes to the destination file as it downloads it, without
// loading the entire file into memory.
func DownloadFile(urlValue, filepath string) error {
// Create the file
//nolint:gosec // path traversal is not a concern here, as the filepath is generated by us and not user input
out, err := os.Create(filepath)
if err != nil {
return err
}
defer func(out *os.File) {
err := out.Close()
if err != nil {
slog.Error("failed to close file", slog.Any("err", err))
}
}(out)
// Get the data
//nolint:gosec,bodyclose // this is a controlled URL, not user input
resp, err := http.Get(urlValue)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
// Write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}