diff --git a/packages/datastream.go b/packages/datastream.go index 889cd94c9..4e383e426 100644 --- a/packages/datastream.go +++ b/packages/datastream.go @@ -72,20 +72,24 @@ type DataStream struct { // Reference to the package containing this data stream packageRef *Package - Deprecated *Deprecated `config:"deprecated,omitempty" json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Deprecated *Deprecated `config:"deprecated,omitempty" json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + IaCBlueprints []IaCBlueprint `config:"iac_blueprints,omitempty" json:"iac_blueprints,omitempty" yaml:"iac_blueprints,omitempty"` + ProviderPermissions []ProviderPermission `config:"provider_permissions,omitempty" json:"provider_permissions,omitempty" yaml:"provider_permissions,omitempty"` } type Input struct { - Type string `config:"type,omitempty" json:"type,omitempty" yaml:"type,omitempty"` - Package string `config:"package,omitempty" json:"package,omitempty" yaml:"package,omitempty"` - Vars []Variable `config:"vars" json:"vars,omitempty" yaml:"vars,omitempty"` - Title string `config:"title" json:"title,omitempty" yaml:"title,omitempty"` - Description string `config:"description" json:"description,omitempty" yaml:"description,omitempty"` - Streams []Stream `config:"streams" json:"streams,omitempty" yaml:"streams,omitempty"` - TemplatePath string `config:"template_path" json:"template_path,omitempty" yaml:"template_path,omitempty"` - InputGroup string `config:"input_group" json:"input_group,omitempty" yaml:"input_group,omitempty"` - DeploymentModes []string `config:"deployment_modes,omitempty" json:"deployment_modes,omitempty" yaml:"deployment_modes,omitempty"` - Deprecated *Deprecated `config:"deprecated,omitempty" json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Type string `config:"type,omitempty" json:"type,omitempty" yaml:"type,omitempty"` + Package string `config:"package,omitempty" json:"package,omitempty" yaml:"package,omitempty"` + Vars []Variable `config:"vars" json:"vars,omitempty" yaml:"vars,omitempty"` + Title string `config:"title" json:"title,omitempty" yaml:"title,omitempty"` + Description string `config:"description" json:"description,omitempty" yaml:"description,omitempty"` + Streams []Stream `config:"streams" json:"streams,omitempty" yaml:"streams,omitempty"` + TemplatePath string `config:"template_path" json:"template_path,omitempty" yaml:"template_path,omitempty"` + InputGroup string `config:"input_group" json:"input_group,omitempty" yaml:"input_group,omitempty"` + DeploymentModes []string `config:"deployment_modes,omitempty" json:"deployment_modes,omitempty" yaml:"deployment_modes,omitempty"` + Deprecated *Deprecated `config:"deprecated,omitempty" json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + IaCBlueprints []IaCBlueprint `config:"iac_blueprints,omitempty" json:"iac_blueprints,omitempty" yaml:"iac_blueprints,omitempty"` + ProviderPermissions []ProviderPermission `config:"provider_permissions,omitempty" json:"provider_permissions,omitempty" yaml:"provider_permissions,omitempty"` } type Stream struct { diff --git a/packages/iac_test.go b/packages/iac_test.go new file mode 100644 index 000000000..9b34af4f6 --- /dev/null +++ b/packages/iac_test.go @@ -0,0 +1,157 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package packages + +import ( + "archive/zip" + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/elastic/package-registry/archiver" +) + +const ( + iacPatchesFile = "iac/account.cloudformation.patches.json" + iacTemplateFile = "iac/account.cloudformation.json" + iacBlueprintID = "aws/federated-identity/account/v1" + iacPatchesJSON = `[{"op":"add","path":"/Resources/ExampleBucket","value":{"Type":"AWS::S3::Bucket"}}]` + iacTemplateJSON = `{"AWSTemplateFormatVersion":"2010-09-09","Resources":{"ExampleBucket":{"Type":"AWS::S3::Bucket"}}}` + iacTestPkgName = "mypkg" + iacTestPkgVer = "1.0.0" +) + +func TestPackageLoadAssetsIncludesIac(t *testing.T) { + pkg := loadIacTestPackage(t) + + assert.True(t, hasAssetSuffix(pkg.Assets, "/"+iacPatchesFile), "assets should include %s, got %v", iacPatchesFile, pkg.Assets) + assert.True(t, hasAssetSuffix(pkg.Assets, "/"+iacTemplateFile), "assets should include %s, got %v", iacTemplateFile, pkg.Assets) +} + +func TestPackageIndexIncludesIaCBlueprints(t *testing.T) { + pkg := loadIacTestPackage(t) + + require.NotEmpty(t, pkg.IaCBlueprints) + assert.Equal(t, iacBlueprintID, pkg.IaCBlueprints[0].ID) + assert.Equal(t, "cloudformation", pkg.IaCBlueprints[0].Format) + assert.Equal(t, iacPatchesFile, pkg.IaCBlueprints[0].Patches) + + data, err := json.Marshal(pkg) + require.NoError(t, err) + + var decoded map[string]interface{} + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + blueprints, ok := decoded["iac_blueprints"].([]interface{}) + require.True(t, ok, "package JSON should include iac_blueprints") + require.NotEmpty(t, blueprints) + first, ok := blueprints[0].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, iacBlueprintID, first["id"]) +} + +func TestServePackageResourceIac(t *testing.T) { + pkg := loadIacTestPackage(t) + + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/package/"+iacTestPkgName+"/"+iacTestPkgVer+"/"+iacPatchesFile, nil) + ServePackageResource(zap.NewNop(), recorder, req, pkg, iacPatchesFile) + + require.Equal(t, http.StatusOK, recorder.Code) + assert.JSONEq(t, iacPatchesJSON, recorder.Body.String()) +} + +func TestArchivePackageIncludesIac(t *testing.T) { + pkg := loadIacTestPackage(t) + + var buf bytes.Buffer + err := archiver.ArchivePackage(&buf, archiver.PackageProperties{ + Name: pkg.Name, + Version: pkg.Version, + Path: pkg.BasePath, + }) + require.NoError(t, err) + + reader, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + require.NoError(t, err) + + root := iacTestPkgName + "-" + iacTestPkgVer + "/" + assert.True(t, zipContains(reader, root+iacPatchesFile), "zip should contain %s", root+iacPatchesFile) + assert.True(t, zipContains(reader, root+iacTemplateFile), "zip should contain %s", root+iacTemplateFile) +} + +func loadIacTestPackage(t *testing.T) *Package { + t.Helper() + pkgDir := createIacTestPackage(t) + p, err := NewPackage(zap.NewNop(), pkgDir, ExtractedFileSystemBuilder) + require.NoError(t, err) + return p +} + +func createIacTestPackage(t *testing.T) string { + t.Helper() + + pkgDir := filepath.Join(t.TempDir(), iacTestPkgName, iacTestPkgVer) + err := os.MkdirAll(filepath.Join(pkgDir, "docs"), 0755) + require.NoError(t, err) + err = os.MkdirAll(filepath.Join(pkgDir, "iac"), 0755) + require.NoError(t, err) + + manifest := `format_version: 1.0.0 +name: ` + iacTestPkgName + ` +title: My Package +description: Test package for iac folder serving +version: ` + iacTestPkgVer + ` +type: integration +owner: + github: elastic +iac_blueprints: + - id: ` + iacBlueprintID + ` + format: cloudformation + patches: ` + iacPatchesFile + ` + title: Account +` + err = os.WriteFile(filepath.Join(pkgDir, "manifest.yml"), []byte(manifest), 0644) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(pkgDir, "docs", "README.md"), []byte("# My Package\n"), 0644) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(pkgDir, filepath.FromSlash(iacPatchesFile)), []byte(iacPatchesJSON), 0644) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(pkgDir, filepath.FromSlash(iacTemplateFile)), []byte(iacTemplateJSON), 0644) + require.NoError(t, err) + + return pkgDir +} + +func hasAssetSuffix(assets []string, suffix string) bool { + for _, a := range assets { + if strings.HasSuffix(a, suffix) { + return true + } + } + return false +} + +func zipContains(reader *zip.Reader, name string) bool { + for _, f := range reader.File { + if f.Name == name { + return true + } + } + return false +} diff --git a/packages/package.go b/packages/package.go index b752af467..596747bce 100644 --- a/packages/package.go +++ b/packages/package.go @@ -40,15 +40,17 @@ type Package struct { BasePackage `config:",inline" json:",inline" yaml:",inline"` FormatVersion string `config:"format_version" json:"format_version" yaml:"format_version"` - Readme *string `config:"readme,omitempty" json:"readme,omitempty" yaml:"readme,omitempty"` - License string `config:"license,omitempty" json:"license,omitempty" yaml:"license,omitempty"` - Screenshots []Image `config:"screenshots,omitempty" json:"screenshots,omitempty" yaml:"screenshots,omitempty"` - Assets []string `config:"assets,omitempty" json:"assets,omitempty" yaml:"assets,omitempty"` - PolicyTemplates []PolicyTemplate `config:"policy_templates,omitempty" json:"policy_templates,omitempty" yaml:"policy_templates,omitempty"` - DataStreams []*DataStream `config:"data_streams,omitempty" json:"data_streams,omitempty" yaml:"data_streams,omitempty"` - Vars []Variable `config:"vars" json:"vars,omitempty" yaml:"vars,omitempty"` - Elasticsearch *PackageElasticsearch `config:"elasticsearch,omitempty" json:"elasticsearch,omitempty" yaml:"elasticsearch,omitempty"` - Agent *PackageAgent `config:"agent,omitempty" json:"agent,omitempty" yaml:"agent,omitempty"` + Readme *string `config:"readme,omitempty" json:"readme,omitempty" yaml:"readme,omitempty"` + License string `config:"license,omitempty" json:"license,omitempty" yaml:"license,omitempty"` + Screenshots []Image `config:"screenshots,omitempty" json:"screenshots,omitempty" yaml:"screenshots,omitempty"` + Assets []string `config:"assets,omitempty" json:"assets,omitempty" yaml:"assets,omitempty"` + PolicyTemplates []PolicyTemplate `config:"policy_templates,omitempty" json:"policy_templates,omitempty" yaml:"policy_templates,omitempty"` + DataStreams []*DataStream `config:"data_streams,omitempty" json:"data_streams,omitempty" yaml:"data_streams,omitempty"` + Vars []Variable `config:"vars" json:"vars,omitempty" yaml:"vars,omitempty"` + Elasticsearch *PackageElasticsearch `config:"elasticsearch,omitempty" json:"elasticsearch,omitempty" yaml:"elasticsearch,omitempty"` + Agent *PackageAgent `config:"agent,omitempty" json:"agent,omitempty" yaml:"agent,omitempty"` + IaCBlueprints []IaCBlueprint `config:"iac_blueprints,omitempty" json:"iac_blueprints,omitempty" yaml:"iac_blueprints,omitempty"` + ProviderPermissions []ProviderPermission `config:"provider_permissions,omitempty" json:"provider_permissions,omitempty" yaml:"provider_permissions,omitempty"` // Local path to the package dir BasePath string `json:"-" yaml:"-"` @@ -130,6 +132,40 @@ type PolicyTemplate struct { IngestionMethod string `config:"ingestion_method,omitempty" json:"ingestion_method,omitempty" yaml:"ingestion_method,omitempty"` TemplatePath string `config:"template_path,omitempty" json:"template_path,omitempty" yaml:"template_path,omitempty"` Deprecated *Deprecated `config:"deprecated,omitempty" json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + + IaCBlueprints []IaCBlueprint `config:"iac_blueprints,omitempty" json:"iac_blueprints,omitempty" yaml:"iac_blueprints,omitempty"` + ProviderPermissions []ProviderPermission `config:"provider_permissions,omitempty" json:"provider_permissions,omitempty" yaml:"provider_permissions,omitempty"` +} + +// IaCBlueprint is a contribution to a shared IaC canonical blueprint. +type IaCBlueprint struct { + ID string `config:"id" json:"id" yaml:"id" validate:"required"` + Format string `config:"format" json:"format" yaml:"format" validate:"required"` + Patches string `config:"patches" json:"patches" yaml:"patches" validate:"required"` + Title string `config:"title,omitempty" json:"title,omitempty" yaml:"title,omitempty"` +} + +// ProviderPermission declares permissions and roles required from a named provider. +type ProviderPermission struct { + Provider string `config:"provider" json:"provider" yaml:"provider" validate:"required"` + Description string `config:"description,omitempty" json:"description,omitempty" yaml:"description,omitempty"` + Roles []ProviderRole `config:"roles,omitempty" json:"roles,omitempty" yaml:"roles,omitempty"` + Permissions []PermissionEntry `config:"permissions,omitempty" json:"permissions,omitempty" yaml:"permissions,omitempty"` +} + +// ProviderRole is a pre-defined role or managed policy to attach. +type ProviderRole struct { + Name string `config:"name" json:"name" yaml:"name" validate:"required"` + ID string `config:"id,omitempty" json:"id,omitempty" yaml:"id,omitempty"` + Description string `config:"description,omitempty" json:"description,omitempty" yaml:"description,omitempty"` +} + +// PermissionEntry is an individual permission grant required by an integration unit. +type PermissionEntry struct { + Name string `config:"name" json:"name" yaml:"name" validate:"required"` + Description string `config:"description,omitempty" json:"description,omitempty" yaml:"description,omitempty"` + Resources []string `config:"resources,omitempty" json:"resources,omitempty" yaml:"resources,omitempty"` + Conditions map[string]interface{} `config:"conditions,omitempty" json:"conditions,omitempty" yaml:"conditions,omitempty"` } // Source contains metadata about the source of the package and its distribution.