diff --git a/pkg/artifactory/datasource_artifactory_file.go b/pkg/artifactory/datasource_artifactory_file.go new file mode 100644 index 00000000..12594796 --- /dev/null +++ b/pkg/artifactory/datasource_artifactory_file.go @@ -0,0 +1,202 @@ +package artifactory + +import ( + "context" + "fmt" + "github.com/atlassian/go-artifactory/v2/artifactory" + "github.com/atlassian/go-artifactory/v2/artifactory/v1" + "github.com/hashicorp/terraform/helper/schema" + "os" + "io" + "crypto/sha256" + "encoding/hex" +) + +func datasourceArtifactoryFile() *schema.Resource { + return &schema.Resource{ + Create: nil, + Read: resourceArtifactRead, + Update: nil, + Delete: nil, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + }, + "path": { + Type: schema.TypeString, + Required: true, + }, + "created": { + Type: schema.TypeString, + Computed: true, + }, + "created_by": { + Type: schema.TypeString, + Computed: true, + }, + "last_modified": { + Type: schema.TypeString, + Computed: true, + }, + "modified_by": { + Type: schema.TypeString, + Computed: true, + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + }, + "download_uri": { + Type: schema.TypeString, + Computed: true, + }, + "mimetype": { + Type: schema.TypeString, + Computed: true, + }, + "size": { + Type: schema.TypeInt, + Computed: true, + }, + "md5": { + Type: schema.TypeString, + Computed: true, + }, + "sha1": { + Type: schema.TypeString, + Computed: true, + }, + "sha256": { + Type: schema.TypeString, + Computed: true, + }, + "output_path": { + Type: schema.TypeString, + Optional: true, + }, + "force_overwrite": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + } +} + +func resourceArtifactRead(d *schema.ResourceData, m interface{}) error { + c := m.(*artifactory.Artifactory) + + repository := d.Get("repository").(string) + path := d.Get("path").(string) + outputPath := d.Get("output_path").(string) + forceOverwrite := d.Get("force_overwrite").(bool) + + fileInfo, _, err := c.V1.Artifacts.FileInfo(context.Background(), repository, path) + if err != nil { + return err + } + + skip, err := SkipDownload(fileInfo, outputPath) + if err != nil && !forceOverwrite { + return err + } + + if !skip { + outFile, err := os.Create(outputPath) + if err != nil { + return err + } + + defer outFile.Close() + + fileInfo, _, err = c.V1.Artifacts.FileContents(context.Background(), repository, path, outFile) + if err != nil { + return err + } + } + + return packFileInfo(fileInfo, d) +} + +func SkipDownload(fileInfo *v1.FileInfo, path string) (bool, error) { + const skip = true + const dontSkip = false + + if path == "" { + // no path specified, nothing to download + return skip, nil + } + + if FileExists(path) { + chks_matches, err := VerifySha256Checksum(path, *fileInfo.Checksums.Sha256) + + if chks_matches { + return skip, nil + } else if err != nil { + return dontSkip, err + } else { + return dontSkip, fmt.Errorf("Local file differs from upstream version") + } + } else { + return dontSkip, nil + } +} + +func FileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +func VerifySha256Checksum(path string, expectedSha256 string) (bool, error) { + f, err := os.Open(path) + if err != nil { + return false, err + } + defer f.Close() + + hasher := sha256.New() + + if _, err := io.Copy(hasher, f); err != nil { + return false, err + } + + return hex.EncodeToString(hasher.Sum(nil)) == expectedSha256, nil +} + +func packFileInfo(fileInfo *v1.FileInfo, d *schema.ResourceData) error { + hasErr := false + logErr := cascadingErr(&hasErr) + + d.SetId(*fileInfo.DownloadUri) + + logErr(d.Set("created", *fileInfo.Created)) + logErr(d.Set("created_by", *fileInfo.CreatedBy)) + logErr(d.Set("last_modified", *fileInfo.LastModified)) + logErr(d.Set("modified_by", *fileInfo.ModifiedBy)) + logErr(d.Set("last_updated", *fileInfo.LastUpdated)) + logErr(d.Set("download_uri", *fileInfo.DownloadUri)) + logErr(d.Set("mimetype", *fileInfo.MimeType)) + logErr(d.Set("size", *fileInfo.Size)) + + if fileInfo.Checksums != nil { + logErr(d.Set("md5", *fileInfo.Checksums.Md5)) + logErr(d.Set("sha1", *fileInfo.Checksums.Sha1)) + logErr(d.Set("sha256", *fileInfo.Checksums.Sha256)) + } + + if hasErr { + return fmt.Errorf("failed to pack fileInfo") + } + + return nil +} \ No newline at end of file diff --git a/pkg/artifactory/datasource_artifactory_file_test.go b/pkg/artifactory/datasource_artifactory_file_test.go new file mode 100644 index 00000000..2ad24c89 --- /dev/null +++ b/pkg/artifactory/datasource_artifactory_file_test.go @@ -0,0 +1,91 @@ +package artifactory + +import ( + "testing" + "io/ioutil" + "os" + "github.com/stretchr/testify/assert" + "github.com/atlassian/go-artifactory/v2/artifactory/v1" + "path/filepath" +) + +func TestSkipDownload(t *testing.T) { + const testString = "test content" + const expectedSha256 = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72" + + file, err := CreateTempFile(testString) + + assert.Nil(t, err) + + defer CloseAndRemove(file) + + existingPath, _ := filepath.Abs(file.Name()) + nonExistingPath := existingPath + "-doesnt-exist" + + sha256 := expectedSha256 + fileInfo := new(v1.FileInfo) + fileInfo.Checksums = new(v1.Checksums) + fileInfo.Checksums.Sha256 = &sha256 + + skip, err := SkipDownload(fileInfo, existingPath) + assert.Equal(t, true, skip) // file exists, checksum matches => skip + assert.Nil(t, err) + + skip, err = SkipDownload(fileInfo, nonExistingPath) + assert.Equal(t, false, skip) // file doesn't exist => dont skip + assert.Nil(t, err) + + sha256 = "6666666666666666666666666666666666666666666666666666666666666666" + fileInfo.Checksums.Sha256 = &sha256 + + skip, err = SkipDownload(fileInfo, existingPath) + assert.Equal(t, false, skip) // file exists, checksum doesnt match => dont skip & err + assert.NotNil(t, err) +} + +func TestFileExists(t *testing.T) { + tmpFile, err := CreateTempFile("test") + + assert.Nil(t, err) + + defer CloseAndRemove(tmpFile) + + existingPath, _ := filepath.Abs(tmpFile.Name()) + nonExistingPath := existingPath + "-doesnt-exist" + + assert.Equal(t, true, FileExists(existingPath)) + assert.Equal(t, false, FileExists(nonExistingPath)) +} + +func TestVerifySha256Checksum(t *testing.T) { + const testString = "test content" + const expectedSha256 = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72" + + file, err := CreateTempFile(testString) + + assert.Nil(t, err) + + defer CloseAndRemove(file) + + filePath, _ := filepath.Abs(file.Name()) + + sha256Verified, err := VerifySha256Checksum(filePath, expectedSha256) + + assert.Nil(t, err) + assert.Equal(t, true, sha256Verified) +} + +func CreateTempFile(content string) (f *os.File, err error) { + file, err := ioutil.TempFile(os.TempDir(), "terraform-provider-artifactory-") + + if content != "" { + file.WriteString(content) + } + + return file, err +} + +func CloseAndRemove(f *os.File) { + f.Close() + os.Remove(f.Name()) +} \ No newline at end of file diff --git a/pkg/artifactory/provider.go b/pkg/artifactory/provider.go index c199bbb2..a7b1e187 100644 --- a/pkg/artifactory/provider.go +++ b/pkg/artifactory/provider.go @@ -72,6 +72,10 @@ func Provider() terraform.ResourceProvider { "artifactory_permission_targets": resourceArtifactoryPermissionTargets(), }, + DataSourcesMap: map[string]*schema.Resource{ + "artifactory_file": datasourceArtifactoryFile(), + }, + ConfigureFunc: providerConfigure, } }