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 }