diff --git a/pkg/tfgen/convert_cli.go b/pkg/tfgen/convert_cli.go index 1e3bd8fad..2588e0a1b 100644 --- a/pkg/tfgen/convert_cli.go +++ b/pkg/tfgen/convert_cli.go @@ -64,14 +64,13 @@ type cliConverter struct { generator interface { convertHCL( - e *Example, hcl, path, exampleTitle string, languages []string, + e *Example, hcl, path string, languages []string, ) (string, error) convertExamplesInner( docs string, path examplePath, - stripSubsectionsWithErrors bool, convertHCL func( - e *Example, hcl, path, exampleTitle string, languages []string, + e *Example, hcl, path string, languages []string, ) (string, error), useCoverageTracker bool, ) string @@ -81,9 +80,8 @@ type cliConverter struct { loader schema.Loader convertExamplesList []struct { - docs string - path examplePath - stripSubsectionsWithErrors bool + docs string + path examplePath } currentPackageSpec *pschema.PackageSpec @@ -124,20 +122,17 @@ func (g *Generator) cliConverter() *cliConverter { func (cc *cliConverter) StartConvertingExamples( docs string, path examplePath, - stripSubsectionsWithErrors bool, ) string { // Record inner HCL conversions and discard the result. cov := false // do not use coverage tracker yet, it will be used in the second pass. - cc.generator.convertExamplesInner(docs, path, stripSubsectionsWithErrors, cc.recordHCL, cov) + cc.generator.convertExamplesInner(docs, path, cc.recordHCL, cov) // Record the convertExamples job for later. e := struct { - docs string - path examplePath - stripSubsectionsWithErrors bool + docs string + path examplePath }{ - docs: docs, - path: path, - stripSubsectionsWithErrors: stripSubsectionsWithErrors, + docs: docs, + path: path, } cc.convertExamplesList = append(cc.convertExamplesList, e) // Return a placeholder referencing the convertExampleJob by position. @@ -165,8 +160,7 @@ func (cc *cliConverter) FinishConvertingExamples(p pschema.PackageSpec) pschema. // Use coverage tracker here on the second pass. useCoverageTracker := true - source := cc.generator.convertExamplesInner(ex.docs, ex.path, - ex.stripSubsectionsWithErrors, cc.generator.convertHCL, useCoverageTracker) + source := cc.generator.convertExamplesInner(ex.docs, ex.path, cc.generator.convertHCL, useCoverageTracker) // JSON-escaping to splice into JSON string literals. bytes, err := json.Marshal(source) contract.AssertNoErrorf(err, "json.Masrhal(sourceCode)") @@ -465,7 +459,7 @@ func (cc *cliConverter) convertPCL( // Act as a convertHCL stub that does not actually convert but spies on the literals involved. func (cc *cliConverter) recordHCL( - e *Example, hcl, path, exampleTitle string, languages []string, + e *Example, hcl, path string, languages []string, ) (string, error) { cache := cc.generator.getOrCreateExamplesCache() diff --git a/pkg/tfgen/docs.go b/pkg/tfgen/docs.go index 2b37aedb1..20ada2f61 100644 --- a/pkg/tfgen/docs.go +++ b/pkg/tfgen/docs.go @@ -16,6 +16,7 @@ package tfgen import ( "bytes" + "crypto/md5" //nolint:gosec "encoding/json" "errors" "fmt" @@ -44,6 +45,11 @@ import ( "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" ) +const ( + startPulumiCodeChooser = "" + endPulumiCodeChooser = "" +) + // argumentDocs contains the documentation metadata for an argument of the resource. type argumentDocs struct { // The description for this argument. @@ -995,7 +1001,7 @@ func (p *tfMarkdownParser) parseImports(subsection []string) { return } - var importDocString []string + var importDocString string for _, section := range subsection { if strings.Contains(section, "**NOTE:") || strings.Contains(section, "**Please Note:") || strings.Contains(section, "**Note:**") { @@ -1045,22 +1051,20 @@ func (p *tfMarkdownParser) parseImports(subsection []string) { } else { tok = "MISSING_TOK" } - // Because splitGroupLines will strip any newlines off our description text, we use `` as a - // placeholder, which we will replace with newlines in convertExamplesInner. - importCommand := fmt.Sprintf("$ pulumi import %s%s", tok, importString) - importDetails := []string{"```sh", importCommand, "```"} - importDocString = append(importDocString, importDetails...) + importCommand := fmt.Sprintf("$ pulumi import %s%s\n", tok, importString) + importDetails := "```sh\n" + importCommand + "```\n\n" + importDocString = importDocString + importDetails } else { if !isBlank(section) { // Ensure every section receives a line break. - section = section + "" - importDocString = append(importDocString, section) + section = section + "\n\n" + importDocString = importDocString + section } } } if len(importDocString) > 0 { - p.ret.Import = fmt.Sprintf("## Import\n\n%s", strings.Join(importDocString, " ")) + p.ret.Import = fmt.Sprintf("## Import\n\n%s", importDocString) } } @@ -1132,9 +1136,9 @@ func tryParseV2Imports(typeToken string, markdownLines []string) (string, bool) } func emitImportCodeBlock(w io.Writer, typeToken, name, id string) { - fmt.Fprintf(w, "```sh\n") + fmt.Fprintf(w, "```sh\n") fmt.Fprintf(w, "$ pulumi import %s %s %s\n", typeToken, name, id) - fmt.Fprintf(w, "```\n") + fmt.Fprintf(w, "```\n") } // Parses import example codeblocks. @@ -1246,7 +1250,7 @@ func (p *tfMarkdownParser) reformatSubsection(lines []string) ([]string, bool, b // convertExamples converts any code snippets in a subsection to Pulumi-compatible code. This conversion is done on a // per-subsection basis; subsections with failing examples will be elided upon the caller's request. -func (g *Generator) convertExamples(docs string, path examplePath, stripSubsectionsWithErrors bool) (result string) { +func (g *Generator) convertExamples(docs string, path examplePath) (result string) { if docs == "" { return "" } @@ -1266,11 +1270,54 @@ func (g *Generator) convertExamples(docs string, path examplePath, stripSubsecti return docs } + // This function is very expensive for large providers. Permit experimental disk-based caching if the user + // specifies the PULUMI_CONVERT_EXAMPES_CACHE_DIR environment variable, pointing to a folder for the cache. + { + dir, enableCache := os.LookupEnv("PULUMI_CONVERT_EXAMPES_CACHE_DIR") + if enableCache && dir != "" { + path := path.String() + sep := string(rune(0)) + var buf bytes.Buffer + fmt.Fprintf(&buf, "provider=%v%s", g.info.Name, sep) + fmt.Fprintf(&buf, "version=%v%s", g.info.Version, sep) + fmt.Fprintf(&buf, "path=%v%s", path, sep) + fmt.Fprintf(&buf, "docs=%v%s", docs, sep) + + hash := fmt.Sprintf("%x", md5.Sum(buf.Bytes())) //nolint:gosec + + filePath := filepath.Join(dir, hash) + + bytes, err := os.ReadFile(filePath) + if err == nil { + // cache hit + return string(bytes) + } + // ignore the error, assume cache miss or file not found + defer func() { + // only write the cache for sizable results, >0.5kb + if len(result) > 512 { + // try to write to the cache + err := os.WriteFile(filePath, []byte(result), 0600) + if err != nil { + panic(fmt.Errorf("failed to write examples-cache: %w", err)) + } + } + }() + } + } + if strings.Contains(docs, "```typescript") || strings.Contains(docs, "```python") || strings.Contains(docs, "```go") || strings.Contains(docs, "```yaml") || strings.Contains(docs, "```csharp") || strings.Contains(docs, "```java") { // we have explicitly rewritten these examples and need to just return them directly rather than trying - // to reconvert them. But we need to surround them in the examples shortcode for rendering on the registry + // to reconvert them. + // + //TODO: This only works if the incoming docs already have an {{% example }} shortcode, and if they are + // in an "Example Usage" section. + // The shortcode should be replaced with the new HTML comment, either in the incoming docs, or here to avoid + // breaking users. + + //We need to surround the examples in the examples shortcode for rendering on the registry // Find the index of "## Example Usage" exampleIndex := strings.Index(docs, "## Example Usage") @@ -1287,13 +1334,82 @@ func (g *Generator) convertExamples(docs string, path examplePath, stripSubsecti } if cliConverterEnabled() { - return g.cliConverter().StartConvertingExamples(docs, path, - stripSubsectionsWithErrors) + return g.cliConverter().StartConvertingExamples(docs, path) } // Use coverage tracker: on by default. cov := true - return g.convertExamplesInner(docs, path, stripSubsectionsWithErrors, g.convertHCL, cov) + return g.convertExamplesInner(docs, path, g.convertHCL, cov) +} + +// codeBlock represents a code block found in the upstream docs, delineated by code fences (```). +// It also tracks which header it is part of. +type codeBlock struct { + start int // The index of the first backtick of an opening code fence + end int // The index of the first backtick of a closing code fence + headerStart int // The index of the first "#" in a Markdown header. A value of -1 indicates there's no header. +} + +func findCodeBlock(doc string, i int) (codeBlock, bool) { + codeFence := "```" + var block codeBlock + //find opening code fence + if doc[i:i+len(codeFence)] == codeFence { + block.start = i + // find closing code fence + for j := i + len(codeFence); j < (len(doc) - len(codeFence)); j++ { + if doc[j:j+len(codeFence)] == codeFence { + block.end = j + return block, true + } + } + return block, false + } + return block, false +} + +func findHeader(doc string, i int) (int, bool) { + h2 := "##" + h3 := "###" + foundH2, foundH3 := false, false + + if i == 0 { + //handle header at very beginning of doc + foundH2 = doc[i:i+len(h2)] == h2 + foundH3 = doc[i:i+len(h3)] == h3 + + } else { + //all other headers must be preceded by a newline + foundH2 = doc[i:i+len(h2)] == h2 && string(doc[i-1]) == "\n" + foundH3 = doc[i:i+len(h3)] == h3 && string(doc[i-1]) == "\n" + } + + if foundH3 { + return i + len(h3), true + } + if foundH2 { + return i + len(h2), true + } + return -1, false +} +func findFencesAndHeaders(doc string) []codeBlock { + codeFence := "```" + var codeBlocks []codeBlock + headerStart := -1 + for i := 0; i < (len(doc) - len(codeFence)); i++ { + block, found := findCodeBlock(doc, i) + if found { + block.headerStart = headerStart + codeBlocks = append(codeBlocks, block) + i = block.end + 1 + } + headerEnd, found := findHeader(doc, i) + if found { + headerStart = i + i = headerEnd + } + } + return codeBlocks } // The inner implementation of examples conversion is parameterized by convertHCL so that it can be @@ -1301,146 +1417,100 @@ func (g *Generator) convertExamples(docs string, path examplePath, stripSubsecti func (g *Generator) convertExamplesInner( docs string, path examplePath, - stripSubsectionsWithErrors bool, convertHCL func( - e *Example, hcl, path, exampleTitle string, languages []string, + e *Example, hcl, path string, languages []string, ) (string, error), useCoverageTracker bool, ) string { output := &bytes.Buffer{} - - writeTrailingNewline := func(buf *bytes.Buffer) { - if b := buf.Bytes(); len(b) > 0 && b[len(b)-1] != '\n' { - buf.WriteByte('\n') - } - } fprintf := func(w io.Writer, f string, args ...interface{}) { _, err := fmt.Fprintf(w, f, args...) contract.IgnoreError(err) } + codeBlocks := findFencesAndHeaders(docs) + codeFence := "```" - for _, section := range splitGroupLines(docs, "## ") { - if len(section) == 0 { - continue - } + // Traverse the code blocks and take appropriate action before appending to output + textStart := 0 + stripSection := false + stripSectionHeader := 0 + for _, tfBlock := range codeBlocks { - isImportSection := false - header, wroteHeader := section[0], false - isFrontMatter, isExampleUsage := !strings.HasPrefix(header, "## "), header == "## Example Usage" + // if the section has a header we append the header after trying to convert the code. + hasHeader := tfBlock.headerStart >= 0 && textStart < tfBlock.headerStart - if stripSubsectionsWithErrors && header == "## Import" { - isImportSection = true - isFrontMatter = false - wroteHeader = true - } + // append non-code text to output + if !stripSection { + end := tfBlock.start + if hasHeader { + end = tfBlock.headerStart + } + fprintf(output, docs[textStart:end]) - sectionStart, sectionEnd := "", "" - if isExampleUsage { - sectionStart, sectionEnd = "{{% examples %}}\n", "{{% /examples %}}" + } else { + // if we are stripping this section and still have the same header, we append nothing and skip to the next + // code block. + if stripSectionHeader == tfBlock.headerStart { + textStart = tfBlock.end + len(codeFence) + continue + } + if stripSectionHeader < tfBlock.headerStart { + stripSection = false + } } - - for _, subsection := range groupLines(section[1:], "### ") { - - // Each `Example ...` section contains one or more examples written in HCL, optionally separated by - // comments about the examples. We will attempt to convert them using our `tf2pulumi` tool, and append - // them to the description. If we can't, we'll simply log a warning and keep moving along. - subsectionOutput := &bytes.Buffer{} - skippedExamples, hasExamples := false, false - inCodeBlock, codeBlockStart := false, 0 - for i, line := range subsection { - if isImportSection { - // we don't want to do anything with the import section - continue - } - if inCodeBlock { - if strings.Index(line, "```") != 0 { - continue - } - - if g.language.shouldConvertExamples() { - hcl := strings.Join(subsection[codeBlockStart+1:i], "\n") - - // We've got some code -- assume it's HCL and try to - // convert it. - var e *Example - if useCoverageTracker { - e = g.coverageTracker.getOrCreateExample( - path.String(), hcl) - } - - exampleTitle := "" - if strings.Contains(subsection[0], "###") { - exampleTitle = strings.Replace(subsection[0], "### ", "", -1) - } - - langs := genLanguageToSlice(g.language) - codeBlock, err := convertHCL(e, hcl, path.String(), - exampleTitle, langs) - - if err != nil { - skippedExamples = true - } else { - fprintf(subsectionOutput, "\n%s", codeBlock) - } - } else { - skippedExamples = true + // find the actual start index of the code + nextNewLine := strings.Index(docs[tfBlock.start:tfBlock.end], "\n") + if nextNewLine == -1 { + // write the line as-is; this is an in-line fence + fprintf(output, docs[tfBlock.start:tfBlock.end]+"```") + } else { + fenceLanguage := docs[tfBlock.start : tfBlock.start+nextNewLine+1] + // Only attempt to convert code blocks that are either explicitly marked as Terraform, or unmarked. + if fenceLanguage == "```terraform\n" || + fenceLanguage == "```hcl\n" || fenceLanguage == "```\n" { + + // generate the code block and append + if g.language.shouldConvertExamples() { + hcl := docs[tfBlock.start+nextNewLine+1 : tfBlock.end] + + // Most of our results should be HCL, so we try to convert it. + var e *Example + if useCoverageTracker { + e = g.coverageTracker.getOrCreateExample( + path.String(), hcl) } - - hasExamples = true - inCodeBlock = false - } else { - if strings.Index(line, "```") == 0 { - inCodeBlock, codeBlockStart = true, i + langs := genLanguageToSlice(g.language) + convertedBlock, err := convertHCL(e, hcl, path.String(), langs) + + if err != nil { + // We do not write this section, ever. + // We have to strip the entire section: any header, the code block, and any surrounding text. + stripSection = true + stripSectionHeader = tfBlock.headerStart } else { - fprintf(subsectionOutput, "\n%s", line) + // append any headers and following text first + if hasHeader { + fprintf(output, docs[tfBlock.headerStart:tfBlock.start]) + } + fprintf(output, startPulumiCodeChooser) + fprintf(output, "\n%s\n", convertedBlock) + fprintf(output, endPulumiCodeChooser) } } - } - if inCodeBlock { - skippedExamples = true - } - - // If the subsection contained skipped examples and the caller has requested that we remove such subsections, - // do not append its text to the output. Note that we never elide front matter. - if skippedExamples && stripSubsectionsWithErrors && !isFrontMatter { - continue - } - - if !wroteHeader { - if output.Len() > 0 { - fprintf(output, "\n") - } - fprintf(output, "%s%s", sectionStart, header) - wroteHeader = true - } - if hasExamples && isExampleUsage { - writeTrailingNewline(output) - fprintf(output, "{{%% example %%}}%s", subsectionOutput.String()) - writeTrailingNewline(output) - fprintf(output, "{{%% /example %%}}") } else { - fprintf(output, "%s", subsectionOutput.String()) - } - } - - if isImportSection { - section[0] = "\n\n## Import" - importDetails := strings.Join(section, " ") - importDetails = strings.Replace(importDetails, " ", "\n\n", -1) - importDetails = strings.Replace(importDetails, "", "\n", -1) - importDetails = strings.Replace(importDetails, " \n", "\n", -1) - fprintf(output, "%s", importDetails) - continue - } - - if !wroteHeader { - if isFrontMatter { - fprintf(output, "%s", header) + // Take already-valid code blocks as-is. + if hasHeader { + fprintf(output, docs[tfBlock.headerStart:tfBlock.start]) + } + fprintf(output, docs[tfBlock.start:tfBlock.end]+"```") } - } else if sectionEnd != "" { - writeTrailingNewline(output) - fprintf(output, "%s", sectionEnd) } + // The non-code text starts up again after the last closing fences + textStart = tfBlock.end + len(codeFence) + } + // Append any remainder of the docs string to the output + if !stripSection { + fprintf(output, docs[textStart:]) } return output.String() } @@ -1692,7 +1762,7 @@ func hclConversionsToString(hclConversions map[string]string) string { // If all languages fail to convert, the returned string will be "" and an error will be returned. // If some languages fail to convert, the returned string contain any successful conversions and no error will be // returned, but conversion failures will be logged via the Generator. -func (g *Generator) convertHCL(e *Example, hcl, path, exampleTitle string, languages []string) (string, error) { +func (g *Generator) convertHCL(e *Example, hcl, path string, languages []string) (string, error) { g.debug("converting HCL for %s", path) // Fixup the HCL as necessary. @@ -1723,13 +1793,8 @@ func (g *Generator) convertHCL(e *Example, hcl, path, exampleTitle string, langu isCompleteFailure := len(failedLangs) == len(languages) if isCompleteFailure { - if exampleTitle == "" { - g.warn(fmt.Sprintf("unable to convert HCL example for Pulumi entity '%s': %v. The example will be dropped "+ - "from any generated docs or SDKs.", path, err)) - } else { - g.warn(fmt.Sprintf("unable to convert HCL example '%s' for Pulumi entity '%s': %v. The example will be "+ - "dropped from any generated docs or SDKs.", exampleTitle, path, err)) - } + g.warn(fmt.Sprintf("unable to convert HCL example for Pulumi entity '%s': %v. The example will be dropped "+ + "from any generated docs or SDKs.", path, err)) return "", err } @@ -1739,15 +1804,9 @@ func (g *Generator) convertHCL(e *Example, hcl, path, exampleTitle string, langu for lang := range failedLangs { failedLangsStrings = append(failedLangsStrings, lang) - if exampleTitle == "" { - g.warn(fmt.Sprintf("unable to convert HCL example for Pulumi entity '%s' in the following language(s): "+ - "%s. Examples for these languages will be dropped from any generated docs or SDKs.", - path, strings.Join(failedLangsStrings, ", "))) - } else { - g.warn(fmt.Sprintf("unable to convert HCL example '%s' for Pulumi entity '%s' in the following language(s): "+ - "%s. Examples for these languages will be dropped from any generated docs or SDKs.", - exampleTitle, path, strings.Join(failedLangsStrings, ", "))) - } + g.warn(fmt.Sprintf("unable to convert HCL example for Pulumi entity '%s' in the following language(s): "+ + "%s. Examples for these languages will be dropped from any generated docs or SDKs.", + path, strings.Join(failedLangsStrings, ", "))) // At least one language out of the given set has been generated, which is considered a success //nolint:ineffassign diff --git a/pkg/tfgen/docs_test.go b/pkg/tfgen/docs_test.go index 60f763e7e..81e30461f 100644 --- a/pkg/tfgen/docs_test.go +++ b/pkg/tfgen/docs_test.go @@ -816,7 +816,7 @@ func TestParseImports_NoOverrides(t *testing.T) { "", }, token: "snowflake:index/accountGrant:AccountGrant", - expected: "## Import\n\nformat is account name | | | privilege | true/false for with_grant_option ```sh $ pulumi import snowflake:index/accountGrant:AccountGrant example 'accountName|||USAGE|true' ```", + expected: "## Import\n\nformat is account name | | | privilege | true/false for with_grant_option\n\n```sh\n$ pulumi import snowflake:index/accountGrant:AccountGrant example 'accountName|||USAGE|true'\n```\n\n", }, { input: []string{ @@ -829,7 +829,7 @@ func TestParseImports_NoOverrides(t *testing.T) { "", }, token: "snowflake:index/apiIntegration:ApiIntegration", - expected: "## Import\n\n```sh $ pulumi import snowflake:index/apiIntegration:ApiIntegration example name ```", + expected: "## Import\n\n```sh\n$ pulumi import snowflake:index/apiIntegration:ApiIntegration example name\n```\n\n", }, { input: []string{ @@ -844,7 +844,7 @@ func TestParseImports_NoOverrides(t *testing.T) { "", }, token: "gcp:accesscontextmanager/accessLevel:AccessLevel", - expected: "## Import\n\nThis is a first line in a multi-line import section * `{{name}}` * `{{id}}` For example: ```sh $ pulumi import gcp:accesscontextmanager/accessLevel:AccessLevel example name ```", + expected: "## Import\n\nThis is a first line in a multi-line import section\n\n* `{{name}}`\n\n* `{{id}}`\n\nFor example:\n\n```sh\n$ pulumi import gcp:accesscontextmanager/accessLevel:AccessLevel example name\n```\n\n", }, { input: readlines(t, "test_data/parse-imports/accessanalyzer.md"), @@ -856,11 +856,6 @@ func TestParseImports_NoOverrides(t *testing.T) { token: "aws:gamelift/matchmakingConfiguration:MatchmakingConfiguration", expectedFile: "test_data/parse-imports/gameliftconfig-expected.md", }, - { - input: readlines(t, "test_data/parse-imports/gameliftconfig.md"), - token: "aws:gamelift/matchmakingConfiguration:MatchmakingConfiguration", - expectedFile: "test_data/parse-imports/gameliftconfig-expected.md", - }, { input: readlines(t, "test_data/parse-imports/lambdalayer.md"), token: "aws:lambda/layerVersion:LayerVersion", @@ -923,8 +918,6 @@ func TestConvertExamples(t *testing.T) { name string path examplePath - stripSubsectionWithErrors bool - needsProviders map[string]pluginDesc } @@ -935,7 +928,6 @@ func TestConvertExamples(t *testing.T) { fullPath: "#/resources/wavefront:index/dashboardJson:DashboardJson", token: "wavefront:index/dashboardJson:DashboardJson", }, - stripSubsectionWithErrors: true, needsProviders: map[string]pluginDesc{ "wavefront": {version: "3.0.0"}, }, @@ -953,6 +945,23 @@ func TestConvertExamples(t *testing.T) { }, }, }, + { + name: "aws_lambda_function", + path: examplePath{ + fullPath: "#/resources/aws:lambda/function:Function", + token: "aws:lambda/function:Function", + }, + needsProviders: map[string]pluginDesc{ + "aws": { + pluginDownloadURL: "github://api.github.com/pulumi", + version: "6.22.2", + }, + "archive": { + pluginDownloadURL: "github://api.github.com/pulumi", + version: "0.0.4", + }, + }, + }, } for _, tc := range testCases { @@ -966,7 +975,75 @@ func TestConvertExamples(t *testing.T) { docs, err := os.ReadFile(filepath.Join("test_data", "convertExamples", fmt.Sprintf("%s.md", tc.name))) require.NoError(t, err) - result := g.convertExamples(string(docs), tc.path, tc.stripSubsectionWithErrors) + result := g.convertExamples(string(docs), tc.path) + + out := filepath.Join("test_data", "convertExamples", + fmt.Sprintf("%s_out.md", tc.name)) + if accept { + err = os.WriteFile(out, []byte(result), 0600) + require.NoError(t, err) + } + expect, err := os.ReadFile(out) + require.NoError(t, err) + assert.Equal(t, string(expect), result) + }) + } +} + +func TestConvertExamplesInner(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("Skipping on windows to avoid failing on incorrect newline handling") + } + + inmem := afero.NewMemMapFs() + info := testprovider.ProviderMiniRandom() + g, err := NewGenerator(GeneratorOptions{ + Package: info.Name, + Version: info.Version, + Language: Schema, + ProviderInfo: info, + Root: inmem, + Sink: diag.DefaultSink(io.Discard, io.Discard, diag.FormatOptions{ + Color: colors.Never, + }), + }) + assert.NoError(t, err) + + type testCase struct { + name string + path examplePath + needsProviders map[string]pluginDesc + } + + testCases := []testCase{ + { + name: "code_tagged_json_stays_in_description", + path: examplePath{ + fullPath: "#/resources/fake:module/resource:Resource", + token: "fake:module/resource:Resource", + }, + }, + { + name: "inline_fences_are_preserved", + path: examplePath{ + fullPath: "#/resources/fake:module/resource:Resource", + token: "fake:module/resource:Resource", + }, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(fmt.Sprintf("%s/setup", tc.name), func(t *testing.T) { + ensureProvidersInstalled(t, tc.needsProviders) + }) + + t.Run(tc.name, func(t *testing.T) { + docs, err := os.ReadFile(filepath.Join("test_data", "convertExamples", + fmt.Sprintf("%s.md", tc.name))) + require.NoError(t, err) + result := g.convertExamplesInner(string(docs), tc.path, g.convertHCL, false) out := filepath.Join("test_data", "convertExamples", fmt.Sprintf("%s_out.md", tc.name)) @@ -986,6 +1063,70 @@ type pluginDesc struct { pluginDownloadURL string } +func TestFindFencesAndHeaders(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("Skipping on windows to avoid failing on incorrect newline handling") + } + type testCase struct { + name string + path string + expected []codeBlock + } + + testCases := []testCase{ + { + name: "finds locations of all fences and headers in a long doc", + path: filepath.Join("test_data", "parse-inner-docs", + "aws_lambda_function_description.md"), + expected: []codeBlock{ + {start: 1966, end: 2977, headerStart: 1947}, + {start: 3001, end: 3224, headerStart: 2982}, + {start: 3387, end: 4105, headerStart: 3229}, + {start: 4358, end: 5953, headerStart: 4110}, + {start: 6622, end: 8041, headerStart: 6421}, + {start: 9151, end: 9238, headerStart: 9052}, + }, + }, + { + name: "finds locations when there are no headers", + path: filepath.Join("test_data", "parse-inner-docs", + "starts-with-code-block.md"), + expected: []codeBlock{ + {start: 0, end: 46, headerStart: -1}, + }, + }, + { + name: "starts with an h2 header", + path: filepath.Join("test_data", "parse-inner-docs", + "starts-with-h2.md"), + expected: []codeBlock{ + {start: 91, end: 142, headerStart: 0}, + }, + }, + { + name: "starts with an h3 header", + path: filepath.Join("test_data", "parse-inner-docs", + "starts-with-h3.md"), + expected: []codeBlock{ + {start: 92, end: 114, headerStart: 0}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + testDocBytes, err := os.ReadFile(tc.path) + require.NoError(t, err) + testDoc := string(testDocBytes) + actual := findFencesAndHeaders(testDoc) + assert.Equal(t, tc.expected, actual) + }) + + } + +} + func ensureProvidersInstalled(t *testing.T, needsProviders map[string]pluginDesc) { pulumi, err := exec.LookPath("pulumi") require.NoError(t, err) diff --git a/pkg/tfgen/generate_schema.go b/pkg/tfgen/generate_schema.go index 95f6fef85..5abbfe663 100644 --- a/pkg/tfgen/generate_schema.go +++ b/pkg/tfgen/generate_schema.go @@ -953,13 +953,13 @@ func (g *schemaGenerator) schemaType(path paths.TypePath, typ *propertyType, out } func (g *Generator) convertExamplesInPropertySpec(path examplePath, spec pschema.PropertySpec) pschema.PropertySpec { - spec.Description = g.convertExamples(spec.Description, path, false) - spec.DeprecationMessage = g.convertExamples(spec.DeprecationMessage, path, false) + spec.Description = g.convertExamples(spec.Description, path) + spec.DeprecationMessage = g.convertExamples(spec.DeprecationMessage, path) return spec } func (g *Generator) convertExamplesInObjectSpec(path examplePath, spec pschema.ObjectTypeSpec) pschema.ObjectTypeSpec { - spec.Description = g.convertExamples(spec.Description, path, false) + spec.Description = g.convertExamples(spec.Description, path) for name, prop := range spec.Properties { spec.Properties[name] = g.convertExamplesInPropertySpec(path.Property(name), prop) } @@ -967,8 +967,8 @@ func (g *Generator) convertExamplesInObjectSpec(path examplePath, spec pschema.O } func (g *Generator) convertExamplesInResourceSpec(path examplePath, spec pschema.ResourceSpec) pschema.ResourceSpec { - spec.Description = g.convertExamples(spec.Description, path, true) - spec.DeprecationMessage = g.convertExamples(spec.DeprecationMessage, path, false) + spec.Description = g.convertExamples(spec.Description, path) + spec.DeprecationMessage = g.convertExamples(spec.DeprecationMessage, path) for name, prop := range spec.Properties { spec.Properties[name] = g.convertExamplesInPropertySpec(path.Property(name), prop) } @@ -983,7 +983,7 @@ func (g *Generator) convertExamplesInResourceSpec(path examplePath, spec pschema } func (g *Generator) convertExamplesInFunctionSpec(path examplePath, spec pschema.FunctionSpec) pschema.FunctionSpec { - spec.Description = g.convertExamples(spec.Description, path, true) + spec.Description = g.convertExamples(spec.Description, path) if spec.Inputs != nil { inputs := g.convertExamplesInObjectSpec(path.Inputs(), *spec.Inputs) spec.Inputs = &inputs diff --git a/pkg/tfgen/test_data/TestConvertViaPulumiCLI/schema.json b/pkg/tfgen/test_data/TestConvertViaPulumiCLI/schema.json index 29d907e92..bb099a89b 100644 --- a/pkg/tfgen/test_data/TestConvertViaPulumiCLI/schema.json +++ b/pkg/tfgen/test_data/TestConvertViaPulumiCLI/schema.json @@ -22,7 +22,7 @@ }, "resources": { "simple:index:resource": { - "description": "{{% examples %}}\n## Example Usage\n{{% example %}}\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as simple from \"@pulumi/simple\";\n\nconst aResource = new simple.Resource(\"a_resource\", {\n renamedInput1: \"hello\",\n inputTwo: \"true\",\n});\nexport const someOutput = aResource.result;\n```\n```python\nimport pulumi\nimport pulumi_simple as simple\n\na_resource = simple.Resource(\"a_resource\",\n renamed_input1=\"hello\",\n input_two=\"true\")\npulumi.export(\"someOutput\", a_resource.result)\n```\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Simple = Pulumi.Simple;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var aResource = new Simple.Resource(\"a_resource\", new()\n {\n RenamedInput1 = \"hello\",\n InputTwo = \"true\",\n });\n\n return new Dictionary\u003cstring, object?\u003e\n {\n [\"someOutput\"] = aResource.Result,\n };\n});\n```\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-simple/sdk/go/simple\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\taResource, err := simple.Newresource(ctx, \"a_resource\", \u0026simple.resourceArgs{\n\t\t\tRenamedInput1: pulumi.String(\"hello\"),\n\t\t\tInputTwo: pulumi.String(\"true\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.Export(\"someOutput\", aResource.Result)\n\t\treturn nil\n\t})\n}\n```\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.simple.resource;\nimport com.pulumi.simple.ResourceArgs;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n var aResource = new Resource(\"aResource\", ResourceArgs.builder() \n .renamedInput1(\"hello\")\n .inputTwo(true)\n .build());\n\n ctx.export(\"someOutput\", aResource.result());\n }\n}\n```\n```yaml\nresources:\n aResource:\n type: simple:resource\n name: a_resource\n properties:\n renamedInput1: hello\n inputTwo: true\noutputs:\n someOutput: ${aResource.result}\n```\n\n##Extras\n{{% /example %}}\n{{% /examples %}}", + "description": "## Example Usage\n\n\u003c!--Start PulumiCodeChooser --\u003e\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as simple from \"@pulumi/simple\";\n\nconst aResource = new simple.Resource(\"a_resource\", {\n renamedInput1: \"hello\",\n inputTwo: \"true\",\n});\nexport const someOutput = aResource.result;\n```\n```python\nimport pulumi\nimport pulumi_simple as simple\n\na_resource = simple.Resource(\"a_resource\",\n renamed_input1=\"hello\",\n input_two=\"true\")\npulumi.export(\"someOutput\", a_resource.result)\n```\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing Simple = Pulumi.Simple;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var aResource = new Simple.Resource(\"a_resource\", new()\n {\n RenamedInput1 = \"hello\",\n InputTwo = \"true\",\n });\n\n return new Dictionary\u003cstring, object?\u003e\n {\n [\"someOutput\"] = aResource.Result,\n };\n});\n```\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-simple/sdk/go/simple\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\taResource, err := simple.Newresource(ctx, \"a_resource\", \u0026simple.resourceArgs{\n\t\t\tRenamedInput1: pulumi.String(\"hello\"),\n\t\t\tInputTwo: pulumi.String(\"true\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.Export(\"someOutput\", aResource.Result)\n\t\treturn nil\n\t})\n}\n```\n```java\npackage generated_program;\n\nimport com.pulumi.Context;\nimport com.pulumi.Pulumi;\nimport com.pulumi.core.Output;\nimport com.pulumi.simple.resource;\nimport com.pulumi.simple.ResourceArgs;\nimport java.util.List;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\npublic class App {\n public static void main(String[] args) {\n Pulumi.run(App::stack);\n }\n\n public static void stack(Context ctx) {\n var aResource = new Resource(\"aResource\", ResourceArgs.builder() \n .renamedInput1(\"hello\")\n .inputTwo(true)\n .build());\n\n ctx.export(\"someOutput\", aResource.result());\n }\n}\n```\n```yaml\nresources:\n aResource:\n type: simple:resource\n name: a_resource\n properties:\n renamedInput1: hello\n inputTwo: true\noutputs:\n someOutput: ${aResource.result}\n```\n\u003c!--End PulumiCodeChooser --\u003e\n\n##Extras\n", "properties": { "inputTwo": { "type": "string" diff --git a/pkg/tfgen/test_data/address_map/expected.json b/pkg/tfgen/test_data/address_map/expected.json index 07752cb92..f4074b30d 100644 --- a/pkg/tfgen/test_data/address_map/expected.json +++ b/pkg/tfgen/test_data/address_map/expected.json @@ -25,5 +25,5 @@ "ips": "The set of IPs on the Address Map.", "memberships": "Zones and Accounts which will be assigned IPs on this Address Map." }, - "Import": "## Import\n\n```sh\u003cbreak\u003e $ pulumi import MISSING_TOK example \u003caccount_id\u003e/\u003caddress_map_id\u003e \u003cbreak\u003e```\u003cbreak\u003e\u003cbreak\u003e" + "Import": "## Import\n\n```sh\n$ pulumi import MISSING_TOK example \u003caccount_id\u003e/\u003caddress_map_id\u003e\n```\n\n" } \ No newline at end of file diff --git a/pkg/tfgen/test_data/azurerm-sql-firewall-rule/expected.json b/pkg/tfgen/test_data/azurerm-sql-firewall-rule/expected.json index 6007e0dc7..4d9bdd0fc 100644 --- a/pkg/tfgen/test_data/azurerm-sql-firewall-rule/expected.json +++ b/pkg/tfgen/test_data/azurerm-sql-firewall-rule/expected.json @@ -20,5 +20,5 @@ "Attributes": { "id": "The SQL Firewall Rule ID." }, - "Import": "## Import\n\nSQL Firewall Rules can be imported using the `resource id`, e.g.\u003cbreak\u003e\u003cbreak\u003e ```sh\u003cbreak\u003e $ pulumi import MISSING_TOK rule1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver/firewallRules/rule1 \u003cbreak\u003e```\u003cbreak\u003e\u003cbreak\u003e" + "Import": "## Import\n\nSQL Firewall Rules can be imported using the `resource id`, e.g.\n\n```sh\n$ pulumi import MISSING_TOK rule1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver/firewallRules/rule1\n```\n\n" } \ No newline at end of file diff --git a/pkg/tfgen/test_data/convertExamples/aws_lambda_function.md b/pkg/tfgen/test_data/convertExamples/aws_lambda_function.md new file mode 100644 index 000000000..655c97e9a --- /dev/null +++ b/pkg/tfgen/test_data/convertExamples/aws_lambda_function.md @@ -0,0 +1,250 @@ +Provides a Lambda Function resource. Lambda allows you to trigger execution of code in response to events in AWS, enabling serverless backend solutions. The Lambda Function itself includes source code and runtime configuration. + +For information about Lambda and how to use it, see [What is AWS Lambda?][1] + +For a detailed example of setting up Lambda and API Gateway, see [Serverless Applications with AWS Lambda and API Gateway.][11] + +~> **NOTE:** Due to [AWS Lambda improved VPC networking changes that began deploying in September 2019](https://aws.amazon.com/blogs/compute/announcing-improved-vpc-networking-for-aws-lambda-functions/), EC2 subnets and security groups associated with Lambda Functions can take up to 45 minutes to successfully delete. + +~> **NOTE:** If you get a `KMSAccessDeniedException: Lambda was unable to decrypt the environment variables because KMS access was denied` error when invoking an `aws_lambda_function` with environment variables, the IAM role associated with the function may have been deleted and recreated _after_ the function was created. You can fix the problem two ways: 1) updating the function's role to another role and then updating it back again to the recreated role, or 2) by using Terraform to `taint` the function and `apply` your configuration again to recreate the function. (When you create a function, Lambda grants permissions on the KMS key to the function's IAM role. If the IAM role is recreated, the grant is no longer valid. Changing the function's role or recreating the function causes Lambda to update the grant.) + +-> To give an external source (like an EventBridge Rule, SNS, or S3) permission to access the Lambda function, use the `aws_lambda_permission` resource. See [Lambda Permission Model][4] for more details. On the other hand, the `role` argument of this resource is the function's execution role for identity and access to AWS services and resources. + +## Example Usage + +### Basic Example + +```terraform +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "archive_file" "lambda" { + type = "zip" + source_file = "lambda.js" + output_path = "lambda_function_payload.zip" +} + +resource "aws_lambda_function" "test_lambda" { + # If the file is not in the current working directory you will need to include a + # path.module in the filename. + filename = "lambda_function_payload.zip" + function_name = "lambda_function_name" + role = aws_iam_role.iam_for_lambda.arn + handler = "index.test" + + source_code_hash = data.archive_file.lambda.output_base64sha256 + + runtime = "nodejs18.x" + + environment { + variables = { + foo = "bar" + } + } +} +``` + +### Lambda Layers + +```terraform +resource "aws_lambda_layer_version" "example" { + # ... other configuration ... +} + +resource "aws_lambda_function" "example" { + # ... other configuration ... + layers = [aws_lambda_layer_version.example.arn] +} +``` + +### Lambda Ephemeral Storage + +Lambda Function Ephemeral Storage(`/tmp`) allows you to configure the storage upto `10` GB. The default value set to `512` MB. + +```terraform +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_lambda_function" "test_lambda" { + filename = "lambda_function_payload.zip" + function_name = "lambda_function_name" + role = aws_iam_role.iam_for_lambda.arn + handler = "index.test" + runtime = "nodejs18.x" + + ephemeral_storage { + size = 10240 # Min 512 MB and the Max 10240 MB + } +} +``` + +### Lambda File Systems + +Lambda File Systems allow you to connect an Amazon Elastic File System (EFS) file system to a Lambda function to share data across function invocations, access existing data including large files, and save function state. + +```terraform +# A lambda function connected to an EFS file system +resource "aws_lambda_function" "example" { + # ... other configuration ... + + file_system_config { + # EFS file system access point ARN + arn = aws_efs_access_point.access_point_for_lambda.arn + + # Local mount path inside the lambda function. Must start with '/mnt/'. + local_mount_path = "/mnt/efs" + } + + vpc_config { + # Every subnet should be able to reach an EFS mount target in the same Availability Zone. Cross-AZ mounts are not permitted. + subnet_ids = [aws_subnet.subnet_for_lambda.id] + security_group_ids = [aws_security_group.sg_for_lambda.id] + } + + # Explicitly declare dependency on EFS mount target. + # When creating or updating Lambda functions, mount target must be in 'available' lifecycle state. + depends_on = [aws_efs_mount_target.alpha] +} + +# EFS file system +resource "aws_efs_file_system" "efs_for_lambda" { + tags = { + Name = "efs_for_lambda" + } +} + +# Mount target connects the file system to the subnet +resource "aws_efs_mount_target" "alpha" { + file_system_id = aws_efs_file_system.efs_for_lambda.id + subnet_id = aws_subnet.subnet_for_lambda.id + security_groups = [aws_security_group.sg_for_lambda.id] +} + +# EFS access point used by lambda file system +resource "aws_efs_access_point" "access_point_for_lambda" { + file_system_id = aws_efs_file_system.efs_for_lambda.id + + root_directory { + path = "/lambda" + creation_info { + owner_gid = 1000 + owner_uid = 1000 + permissions = "777" + } + } + + posix_user { + gid = 1000 + uid = 1000 + } +} +``` + +### Lambda retries + +Lambda Functions allow you to configure error handling for asynchronous invocation. The settings that it supports are `Maximum age of event` and `Retry attempts` as stated in [Lambda documentation for Configuring error handling for asynchronous invocation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-async-errors). To configure these settings, refer to the aws_lambda_function_event_invoke_config resource. + +## CloudWatch Logging and Permissions + +For more information about CloudWatch Logs for Lambda, see the [Lambda User Guide](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions-logs.html). + +```terraform +variable "lambda_function_name" { + default = "lambda_function_name" +} + +resource "aws_lambda_function" "test_lambda" { + function_name = var.lambda_function_name + + # Advanced logging controls (optional) + logging_config { + log_format = "Text" + } + + # ... other configuration ... + depends_on = [ + aws_iam_role_policy_attachment.lambda_logs, + aws_cloudwatch_log_group.example, + ] +} + +# This is to optionally manage the CloudWatch Log Group for the Lambda Function. +# If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. +resource "aws_cloudwatch_log_group" "example" { + name = "/aws/lambda/${var.lambda_function_name}" + retention_in_days = 14 +} + +# See also the following AWS managed policy: AWSLambdaBasicExecutionRole +data "aws_iam_policy_document" "lambda_logging" { + statement { + effect = "Allow" + + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + + resources = ["arn:aws:logs:*:*:*"] + } +} + +resource "aws_iam_policy" "lambda_logging" { + name = "lambda_logging" + path = "/" + description = "IAM policy for logging from a lambda" + policy = data.aws_iam_policy_document.lambda_logging.json +} + +resource "aws_iam_role_policy_attachment" "lambda_logs" { + role = aws_iam_role.iam_for_lambda.name + policy_arn = aws_iam_policy.lambda_logging.arn +} +``` + +## Specifying the Deployment Package + +AWS Lambda expects source code to be provided as a deployment package whose structure varies depending on which `runtime` is in use. See [Runtimes](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime) for the valid values of `runtime`. The expected structure of the deployment package can be found in [the AWS Lambda documentation for each runtime](https://docs.aws.amazon.com/lambda/latest/dg/deployment-package-v2.html). + +Once you have created your deployment package you can specify it either directly as a local file (using the `filename` argument) or indirectly via Amazon S3 (using the `s3_bucket`, `s3_key` and `s3_object_version` arguments). When providing the deployment package via S3 it may be useful to use the `aws_s3_object` resource to upload it. + +For larger deployment packages it is recommended by Amazon to upload via S3, since the S3 API has better support for uploading large files efficiently. + +## Import + +Using `pulumi import`, import Lambda Functions using the `function_name`. For example: + +```sh +$ pulumi import aws:lambda/function:Function test_lambda my_test_lambda_function +``` diff --git a/pkg/tfgen/test_data/convertExamples/aws_lambda_function_out.md b/pkg/tfgen/test_data/convertExamples/aws_lambda_function_out.md new file mode 100644 index 000000000..3e45f325f --- /dev/null +++ b/pkg/tfgen/test_data/convertExamples/aws_lambda_function_out.md @@ -0,0 +1,1395 @@ +Provides a Lambda Function resource. Lambda allows you to trigger execution of code in response to events in AWS, enabling serverless backend solutions. The Lambda Function itself includes source code and runtime configuration. + +For information about Lambda and how to use it, see [What is AWS Lambda?][1] + +For a detailed example of setting up Lambda and API Gateway, see [Serverless Applications with AWS Lambda and API Gateway.][11] + +~> **NOTE:** Due to [AWS Lambda improved VPC networking changes that began deploying in September 2019](https://aws.amazon.com/blogs/compute/announcing-improved-vpc-networking-for-aws-lambda-functions/), EC2 subnets and security groups associated with Lambda Functions can take up to 45 minutes to successfully delete. + +~> **NOTE:** If you get a `KMSAccessDeniedException: Lambda was unable to decrypt the environment variables because KMS access was denied` error when invoking an `aws_lambda_function` with environment variables, the IAM role associated with the function may have been deleted and recreated _after_ the function was created. You can fix the problem two ways: 1) updating the function's role to another role and then updating it back again to the recreated role, or 2) by using Terraform to `taint` the function and `apply` your configuration again to recreate the function. (When you create a function, Lambda grants permissions on the KMS key to the function's IAM role. If the IAM role is recreated, the grant is no longer valid. Changing the function's role or recreating the function causes Lambda to update the grant.) + +-> To give an external source (like an EventBridge Rule, SNS, or S3) permission to access the Lambda function, use the `aws_lambda_permission` resource. See [Lambda Permission Model][4] for more details. On the other hand, the `role` argument of this resource is the function's execution role for identity and access to AWS services and resources. + +## Example Usage + +### Basic Example + + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as archive from "@pulumi/archive"; +import * as aws from "@pulumi/aws"; + +const assumeRole = aws.iam.getPolicyDocument({ + statements: [{ + effect: "Allow", + principals: [{ + type: "Service", + identifiers: ["lambda.amazonaws.com"], + }], + actions: ["sts:AssumeRole"], + }], +}); +const iamForLambda = new aws.iam.Role("iamForLambda", {assumeRolePolicy: assumeRole.then(assumeRole => assumeRole.json)}); +const lambda = archive.getFile({ + type: "zip", + sourceFile: "lambda.js", + outputPath: "lambda_function_payload.zip", +}); +const testLambda = new aws.lambda.Function("testLambda", { + code: new pulumi.asset.FileArchive("lambda_function_payload.zip"), + role: iamForLambda.arn, + handler: "index.test", + runtime: "nodejs18.x", + environment: { + variables: { + foo: "bar", + }, + }, +}); +``` +```python +import pulumi +import pulumi_archive as archive +import pulumi_aws as aws + +assume_role = aws.iam.get_policy_document(statements=[aws.iam.GetPolicyDocumentStatementArgs( + effect="Allow", + principals=[aws.iam.GetPolicyDocumentStatementPrincipalArgs( + type="Service", + identifiers=["lambda.amazonaws.com"], + )], + actions=["sts:AssumeRole"], +)]) +iam_for_lambda = aws.iam.Role("iamForLambda", assume_role_policy=assume_role.json) +lambda_ = archive.get_file(type="zip", + source_file="lambda.js", + output_path="lambda_function_payload.zip") +test_lambda = aws.lambda_.Function("testLambda", + code=pulumi.FileArchive("lambda_function_payload.zip"), + role=iam_for_lambda.arn, + handler="index.test", + runtime="nodejs18.x", + environment=aws.lambda_.FunctionEnvironmentArgs( + variables={ + "foo": "bar", + }, + )) +``` +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Archive = Pulumi.Archive; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var assumeRole = Aws.Iam.GetPolicyDocument.Invoke(new() + { + Statements = new[] + { + new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs + { + Effect = "Allow", + Principals = new[] + { + new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs + { + Type = "Service", + Identifiers = new[] + { + "lambda.amazonaws.com", + }, + }, + }, + Actions = new[] + { + "sts:AssumeRole", + }, + }, + }, + }); + + var iamForLambda = new Aws.Iam.Role("iamForLambda", new() + { + AssumeRolePolicy = assumeRole.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json), + }); + + var lambda = Archive.GetFile.Invoke(new() + { + Type = "zip", + SourceFile = "lambda.js", + OutputPath = "lambda_function_payload.zip", + }); + + var testLambda = new Aws.Lambda.Function("testLambda", new() + { + Code = new FileArchive("lambda_function_payload.zip"), + Role = iamForLambda.Arn, + Handler = "index.test", + Runtime = "nodejs18.x", + Environment = new Aws.Lambda.Inputs.FunctionEnvironmentArgs + { + Variables = + { + { "foo", "bar" }, + }, + }, + }); + +}); +``` +```go +package main + +import ( + "github.com/pulumi/pulumi-archive/sdk/go/archive" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + assumeRole, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{ + Statements: []iam.GetPolicyDocumentStatement{ + { + Effect: pulumi.StringRef("Allow"), + Principals: []iam.GetPolicyDocumentStatementPrincipal{ + { + Type: "Service", + Identifiers: []string{ + "lambda.amazonaws.com", + }, + }, + }, + Actions: []string{ + "sts:AssumeRole", + }, + }, + }, + }, nil) + if err != nil { + return err + } + iamForLambda, err := iam.NewRole(ctx, "iamForLambda", &iam.RoleArgs{ + AssumeRolePolicy: *pulumi.String(assumeRole.Json), + }) + if err != nil { + return err + } + _, err = archive.LookupFile(ctx, &archive.LookupFileArgs{ + Type: "zip", + SourceFile: pulumi.StringRef("lambda.js"), + OutputPath: "lambda_function_payload.zip", + }, nil) + if err != nil { + return err + } + _, err = lambda.NewFunction(ctx, "testLambda", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive("lambda_function_payload.zip"), + Role: iamForLambda.Arn, + Handler: pulumi.String("index.test"), + Runtime: pulumi.String("nodejs18.x"), + Environment: &lambda.FunctionEnvironmentArgs{ + Variables: pulumi.StringMap{ + "foo": pulumi.String("bar"), + }, + }, + }) + if err != nil { + return err + } + return nil + }) +} +``` +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.aws.iam.IamFunctions; +import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs; +import com.pulumi.aws.iam.Role; +import com.pulumi.aws.iam.RoleArgs; +import com.pulumi.archive.ArchiveFunctions; +import com.pulumi.archive.inputs.GetFileArgs; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import com.pulumi.aws.lambda.inputs.FunctionEnvironmentArgs; +import com.pulumi.asset.FileArchive; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + final var assumeRole = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder() + .statements(GetPolicyDocumentStatementArgs.builder() + .effect("Allow") + .principals(GetPolicyDocumentStatementPrincipalArgs.builder() + .type("Service") + .identifiers("lambda.amazonaws.com") + .build()) + .actions("sts:AssumeRole") + .build()) + .build()); + + var iamForLambda = new Role("iamForLambda", RoleArgs.builder() + .assumeRolePolicy(assumeRole.applyValue(getPolicyDocumentResult -> getPolicyDocumentResult.json())) + .build()); + + final var lambda = ArchiveFunctions.getFile(GetFileArgs.builder() + .type("zip") + .sourceFile("lambda.js") + .outputPath("lambda_function_payload.zip") + .build()); + + var testLambda = new Function("testLambda", FunctionArgs.builder() + .code(new FileArchive("lambda_function_payload.zip")) + .role(iamForLambda.arn()) + .handler("index.test") + .runtime("nodejs18.x") + .environment(FunctionEnvironmentArgs.builder() + .variables(Map.of("foo", "bar")) + .build()) + .build()); + + } +} +``` +```yaml +resources: + iamForLambda: + type: aws:iam:Role + properties: + assumeRolePolicy: ${assumeRole.json} + testLambda: + type: aws:lambda:Function + properties: + # If the file is not in the current working directory you will need to include a + # # path.module in the filename. + code: + fn::FileArchive: lambda_function_payload.zip + role: ${iamForLambda.arn} + handler: index.test + runtime: nodejs18.x + environment: + variables: + foo: bar +variables: + assumeRole: + fn::invoke: + Function: aws:iam:getPolicyDocument + Arguments: + statements: + - effect: Allow + principals: + - type: Service + identifiers: + - lambda.amazonaws.com + actions: + - sts:AssumeRole + lambda: + fn::invoke: + Function: archive:getFile + Arguments: + type: zip + sourceFile: lambda.js + outputPath: lambda_function_payload.zip +``` + + +### Lambda Layers + + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const exampleLayerVersion = new aws.lambda.LayerVersion("exampleLayerVersion", {}); +// ... other configuration ... +const exampleFunction = new aws.lambda.Function("exampleFunction", {layers: [exampleLayerVersion.arn]}); +``` +```python +import pulumi +import pulumi_aws as aws + +example_layer_version = aws.lambda_.LayerVersion("exampleLayerVersion") +# ... other configuration ... +example_function = aws.lambda_.Function("exampleFunction", layers=[example_layer_version.arn]) +``` +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var exampleLayerVersion = new Aws.Lambda.LayerVersion("exampleLayerVersion"); + + // ... other configuration ... + var exampleFunction = new Aws.Lambda.Function("exampleFunction", new() + { + Layers = new[] + { + exampleLayerVersion.Arn, + }, + }); + +}); +``` +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + exampleLayerVersion, err := lambda.NewLayerVersion(ctx, "exampleLayerVersion", nil) + if err != nil { + return err + } + _, err = lambda.NewFunction(ctx, "exampleFunction", &lambda.FunctionArgs{ + Layers: pulumi.StringArray{ + exampleLayerVersion.Arn, + }, + }) + if err != nil { + return err + } + return nil + }) +} +``` +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.aws.lambda.LayerVersion; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var exampleLayerVersion = new LayerVersion("exampleLayerVersion"); + + var exampleFunction = new Function("exampleFunction", FunctionArgs.builder() + .layers(exampleLayerVersion.arn()) + .build()); + + } +} +``` +```yaml +resources: + exampleLayerVersion: + type: aws:lambda:LayerVersion + exampleFunction: + type: aws:lambda:Function + properties: + # ... other configuration ... + layers: + - ${exampleLayerVersion.arn} +``` + + +### Lambda Ephemeral Storage + +Lambda Function Ephemeral Storage(`/tmp`) allows you to configure the storage upto `10` GB. The default value set to `512` MB. + + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const assumeRole = aws.iam.getPolicyDocument({ + statements: [{ + effect: "Allow", + principals: [{ + type: "Service", + identifiers: ["lambda.amazonaws.com"], + }], + actions: ["sts:AssumeRole"], + }], +}); +const iamForLambda = new aws.iam.Role("iamForLambda", {assumeRolePolicy: assumeRole.then(assumeRole => assumeRole.json)}); +const testLambda = new aws.lambda.Function("testLambda", { + code: new pulumi.asset.FileArchive("lambda_function_payload.zip"), + role: iamForLambda.arn, + handler: "index.test", + runtime: "nodejs18.x", + ephemeralStorage: { + size: 10240, + }, +}); +``` +```python +import pulumi +import pulumi_aws as aws + +assume_role = aws.iam.get_policy_document(statements=[aws.iam.GetPolicyDocumentStatementArgs( + effect="Allow", + principals=[aws.iam.GetPolicyDocumentStatementPrincipalArgs( + type="Service", + identifiers=["lambda.amazonaws.com"], + )], + actions=["sts:AssumeRole"], +)]) +iam_for_lambda = aws.iam.Role("iamForLambda", assume_role_policy=assume_role.json) +test_lambda = aws.lambda_.Function("testLambda", + code=pulumi.FileArchive("lambda_function_payload.zip"), + role=iam_for_lambda.arn, + handler="index.test", + runtime="nodejs18.x", + ephemeral_storage=aws.lambda_.FunctionEphemeralStorageArgs( + size=10240, + )) +``` +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var assumeRole = Aws.Iam.GetPolicyDocument.Invoke(new() + { + Statements = new[] + { + new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs + { + Effect = "Allow", + Principals = new[] + { + new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs + { + Type = "Service", + Identifiers = new[] + { + "lambda.amazonaws.com", + }, + }, + }, + Actions = new[] + { + "sts:AssumeRole", + }, + }, + }, + }); + + var iamForLambda = new Aws.Iam.Role("iamForLambda", new() + { + AssumeRolePolicy = assumeRole.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json), + }); + + var testLambda = new Aws.Lambda.Function("testLambda", new() + { + Code = new FileArchive("lambda_function_payload.zip"), + Role = iamForLambda.Arn, + Handler = "index.test", + Runtime = "nodejs18.x", + EphemeralStorage = new Aws.Lambda.Inputs.FunctionEphemeralStorageArgs + { + Size = 10240, + }, + }); + +}); +``` +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + assumeRole, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{ + Statements: []iam.GetPolicyDocumentStatement{ + { + Effect: pulumi.StringRef("Allow"), + Principals: []iam.GetPolicyDocumentStatementPrincipal{ + { + Type: "Service", + Identifiers: []string{ + "lambda.amazonaws.com", + }, + }, + }, + Actions: []string{ + "sts:AssumeRole", + }, + }, + }, + }, nil) + if err != nil { + return err + } + iamForLambda, err := iam.NewRole(ctx, "iamForLambda", &iam.RoleArgs{ + AssumeRolePolicy: *pulumi.String(assumeRole.Json), + }) + if err != nil { + return err + } + _, err = lambda.NewFunction(ctx, "testLambda", &lambda.FunctionArgs{ + Code: pulumi.NewFileArchive("lambda_function_payload.zip"), + Role: iamForLambda.Arn, + Handler: pulumi.String("index.test"), + Runtime: pulumi.String("nodejs18.x"), + EphemeralStorage: &lambda.FunctionEphemeralStorageArgs{ + Size: pulumi.Int(10240), + }, + }) + if err != nil { + return err + } + return nil + }) +} +``` +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.aws.iam.IamFunctions; +import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs; +import com.pulumi.aws.iam.Role; +import com.pulumi.aws.iam.RoleArgs; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import com.pulumi.aws.lambda.inputs.FunctionEphemeralStorageArgs; +import com.pulumi.asset.FileArchive; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + final var assumeRole = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder() + .statements(GetPolicyDocumentStatementArgs.builder() + .effect("Allow") + .principals(GetPolicyDocumentStatementPrincipalArgs.builder() + .type("Service") + .identifiers("lambda.amazonaws.com") + .build()) + .actions("sts:AssumeRole") + .build()) + .build()); + + var iamForLambda = new Role("iamForLambda", RoleArgs.builder() + .assumeRolePolicy(assumeRole.applyValue(getPolicyDocumentResult -> getPolicyDocumentResult.json())) + .build()); + + var testLambda = new Function("testLambda", FunctionArgs.builder() + .code(new FileArchive("lambda_function_payload.zip")) + .role(iamForLambda.arn()) + .handler("index.test") + .runtime("nodejs18.x") + .ephemeralStorage(FunctionEphemeralStorageArgs.builder() + .size(10240) + .build()) + .build()); + + } +} +``` +```yaml +resources: + iamForLambda: + type: aws:iam:Role + properties: + assumeRolePolicy: ${assumeRole.json} + testLambda: + type: aws:lambda:Function + properties: + code: + fn::FileArchive: lambda_function_payload.zip + role: ${iamForLambda.arn} + handler: index.test + runtime: nodejs18.x + ephemeralStorage: + size: 10240 +variables: + assumeRole: + fn::invoke: + Function: aws:iam:getPolicyDocument + Arguments: + statements: + - effect: Allow + principals: + - type: Service + identifiers: + - lambda.amazonaws.com + actions: + - sts:AssumeRole +``` + + +### Lambda File Systems + +Lambda File Systems allow you to connect an Amazon Elastic File System (EFS) file system to a Lambda function to share data across function invocations, access existing data including large files, and save function state. + + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +// EFS file system +const efsForLambda = new aws.efs.FileSystem("efsForLambda", {tags: { + Name: "efs_for_lambda", +}}); +// Mount target connects the file system to the subnet +const alpha = new aws.efs.MountTarget("alpha", { + fileSystemId: efsForLambda.id, + subnetId: aws_subnet.subnet_for_lambda.id, + securityGroups: [aws_security_group.sg_for_lambda.id], +}); +// EFS access point used by lambda file system +const accessPointForLambda = new aws.efs.AccessPoint("accessPointForLambda", { + fileSystemId: efsForLambda.id, + rootDirectory: { + path: "/lambda", + creationInfo: { + ownerGid: 1000, + ownerUid: 1000, + permissions: "777", + }, + }, + posixUser: { + gid: 1000, + uid: 1000, + }, +}); +// A lambda function connected to an EFS file system +// ... other configuration ... +const example = new aws.lambda.Function("example", { + fileSystemConfig: { + arn: accessPointForLambda.arn, + localMountPath: "/mnt/efs", + }, + vpcConfig: { + subnetIds: [aws_subnet.subnet_for_lambda.id], + securityGroupIds: [aws_security_group.sg_for_lambda.id], + }, +}, { + dependsOn: [alpha], +}); +``` +```python +import pulumi +import pulumi_aws as aws + +# EFS file system +efs_for_lambda = aws.efs.FileSystem("efsForLambda", tags={ + "Name": "efs_for_lambda", +}) +# Mount target connects the file system to the subnet +alpha = aws.efs.MountTarget("alpha", + file_system_id=efs_for_lambda.id, + subnet_id=aws_subnet["subnet_for_lambda"]["id"], + security_groups=[aws_security_group["sg_for_lambda"]["id"]]) +# EFS access point used by lambda file system +access_point_for_lambda = aws.efs.AccessPoint("accessPointForLambda", + file_system_id=efs_for_lambda.id, + root_directory=aws.efs.AccessPointRootDirectoryArgs( + path="/lambda", + creation_info=aws.efs.AccessPointRootDirectoryCreationInfoArgs( + owner_gid=1000, + owner_uid=1000, + permissions="777", + ), + ), + posix_user=aws.efs.AccessPointPosixUserArgs( + gid=1000, + uid=1000, + )) +# A lambda function connected to an EFS file system +# ... other configuration ... +example = aws.lambda_.Function("example", + file_system_config=aws.lambda_.FunctionFileSystemConfigArgs( + arn=access_point_for_lambda.arn, + local_mount_path="/mnt/efs", + ), + vpc_config=aws.lambda_.FunctionVpcConfigArgs( + subnet_ids=[aws_subnet["subnet_for_lambda"]["id"]], + security_group_ids=[aws_security_group["sg_for_lambda"]["id"]], + ), + opts=pulumi.ResourceOptions(depends_on=[alpha])) +``` +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + // EFS file system + var efsForLambda = new Aws.Efs.FileSystem("efsForLambda", new() + { + Tags = + { + { "Name", "efs_for_lambda" }, + }, + }); + + // Mount target connects the file system to the subnet + var alpha = new Aws.Efs.MountTarget("alpha", new() + { + FileSystemId = efsForLambda.Id, + SubnetId = aws_subnet.Subnet_for_lambda.Id, + SecurityGroups = new[] + { + aws_security_group.Sg_for_lambda.Id, + }, + }); + + // EFS access point used by lambda file system + var accessPointForLambda = new Aws.Efs.AccessPoint("accessPointForLambda", new() + { + FileSystemId = efsForLambda.Id, + RootDirectory = new Aws.Efs.Inputs.AccessPointRootDirectoryArgs + { + Path = "/lambda", + CreationInfo = new Aws.Efs.Inputs.AccessPointRootDirectoryCreationInfoArgs + { + OwnerGid = 1000, + OwnerUid = 1000, + Permissions = "777", + }, + }, + PosixUser = new Aws.Efs.Inputs.AccessPointPosixUserArgs + { + Gid = 1000, + Uid = 1000, + }, + }); + + // A lambda function connected to an EFS file system + // ... other configuration ... + var example = new Aws.Lambda.Function("example", new() + { + FileSystemConfig = new Aws.Lambda.Inputs.FunctionFileSystemConfigArgs + { + Arn = accessPointForLambda.Arn, + LocalMountPath = "/mnt/efs", + }, + VpcConfig = new Aws.Lambda.Inputs.FunctionVpcConfigArgs + { + SubnetIds = new[] + { + aws_subnet.Subnet_for_lambda.Id, + }, + SecurityGroupIds = new[] + { + aws_security_group.Sg_for_lambda.Id, + }, + }, + }, new CustomResourceOptions + { + DependsOn = new[] + { + alpha, + }, + }); + +}); +``` +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/efs" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + // EFS file system + efsForLambda, err := efs.NewFileSystem(ctx, "efsForLambda", &efs.FileSystemArgs{ + Tags: pulumi.StringMap{ + "Name": pulumi.String("efs_for_lambda"), + }, + }) + if err != nil { + return err + } + // Mount target connects the file system to the subnet + alpha, err := efs.NewMountTarget(ctx, "alpha", &efs.MountTargetArgs{ + FileSystemId: efsForLambda.ID(), + SubnetId: pulumi.Any(aws_subnet.Subnet_for_lambda.Id), + SecurityGroups: pulumi.StringArray{ + aws_security_group.Sg_for_lambda.Id, + }, + }) + if err != nil { + return err + } + // EFS access point used by lambda file system + accessPointForLambda, err := efs.NewAccessPoint(ctx, "accessPointForLambda", &efs.AccessPointArgs{ + FileSystemId: efsForLambda.ID(), + RootDirectory: &efs.AccessPointRootDirectoryArgs{ + Path: pulumi.String("/lambda"), + CreationInfo: &efs.AccessPointRootDirectoryCreationInfoArgs{ + OwnerGid: pulumi.Int(1000), + OwnerUid: pulumi.Int(1000), + Permissions: pulumi.String("777"), + }, + }, + PosixUser: &efs.AccessPointPosixUserArgs{ + Gid: pulumi.Int(1000), + Uid: pulumi.Int(1000), + }, + }) + if err != nil { + return err + } + // A lambda function connected to an EFS file system + // ... other configuration ... + _, err = lambda.NewFunction(ctx, "example", &lambda.FunctionArgs{ + FileSystemConfig: &lambda.FunctionFileSystemConfigArgs{ + Arn: accessPointForLambda.Arn, + LocalMountPath: pulumi.String("/mnt/efs"), + }, + VpcConfig: &lambda.FunctionVpcConfigArgs{ + SubnetIds: pulumi.StringArray{ + aws_subnet.Subnet_for_lambda.Id, + }, + SecurityGroupIds: pulumi.StringArray{ + aws_security_group.Sg_for_lambda.Id, + }, + }, + }, pulumi.DependsOn([]pulumi.Resource{ + alpha, + })) + if err != nil { + return err + } + return nil + }) +} +``` +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.aws.efs.FileSystem; +import com.pulumi.aws.efs.FileSystemArgs; +import com.pulumi.aws.efs.MountTarget; +import com.pulumi.aws.efs.MountTargetArgs; +import com.pulumi.aws.efs.AccessPoint; +import com.pulumi.aws.efs.AccessPointArgs; +import com.pulumi.aws.efs.inputs.AccessPointRootDirectoryArgs; +import com.pulumi.aws.efs.inputs.AccessPointRootDirectoryCreationInfoArgs; +import com.pulumi.aws.efs.inputs.AccessPointPosixUserArgs; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import com.pulumi.aws.lambda.inputs.FunctionFileSystemConfigArgs; +import com.pulumi.aws.lambda.inputs.FunctionVpcConfigArgs; +import com.pulumi.resources.CustomResourceOptions; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + var efsForLambda = new FileSystem("efsForLambda", FileSystemArgs.builder() + .tags(Map.of("Name", "efs_for_lambda")) + .build()); + + var alpha = new MountTarget("alpha", MountTargetArgs.builder() + .fileSystemId(efsForLambda.id()) + .subnetId(aws_subnet.subnet_for_lambda().id()) + .securityGroups(aws_security_group.sg_for_lambda().id()) + .build()); + + var accessPointForLambda = new AccessPoint("accessPointForLambda", AccessPointArgs.builder() + .fileSystemId(efsForLambda.id()) + .rootDirectory(AccessPointRootDirectoryArgs.builder() + .path("/lambda") + .creationInfo(AccessPointRootDirectoryCreationInfoArgs.builder() + .ownerGid(1000) + .ownerUid(1000) + .permissions("777") + .build()) + .build()) + .posixUser(AccessPointPosixUserArgs.builder() + .gid(1000) + .uid(1000) + .build()) + .build()); + + var example = new Function("example", FunctionArgs.builder() + .fileSystemConfig(FunctionFileSystemConfigArgs.builder() + .arn(accessPointForLambda.arn()) + .localMountPath("/mnt/efs") + .build()) + .vpcConfig(FunctionVpcConfigArgs.builder() + .subnetIds(aws_subnet.subnet_for_lambda().id()) + .securityGroupIds(aws_security_group.sg_for_lambda().id()) + .build()) + .build(), CustomResourceOptions.builder() + .dependsOn(alpha) + .build()); + + } +} +``` +```yaml +resources: + # A lambda function connected to an EFS file system + example: + type: aws:lambda:Function + properties: + fileSystemConfig: + arn: ${accessPointForLambda.arn} + localMountPath: /mnt/efs + vpcConfig: + subnetIds: + - ${aws_subnet.subnet_for_lambda.id} + securityGroupIds: + - ${aws_security_group.sg_for_lambda.id} + options: + dependson: + - ${alpha} + # EFS file system + efsForLambda: + type: aws:efs:FileSystem + properties: + tags: + Name: efs_for_lambda + # Mount target connects the file system to the subnet + alpha: + type: aws:efs:MountTarget + properties: + fileSystemId: ${efsForLambda.id} + subnetId: ${aws_subnet.subnet_for_lambda.id} + securityGroups: + - ${aws_security_group.sg_for_lambda.id} + # EFS access point used by lambda file system + accessPointForLambda: + type: aws:efs:AccessPoint + properties: + fileSystemId: ${efsForLambda.id} + rootDirectory: + path: /lambda + creationInfo: + ownerGid: 1000 + ownerUid: 1000 + permissions: '777' + posixUser: + gid: 1000 + uid: 1000 +``` + + +### Lambda retries + +Lambda Functions allow you to configure error handling for asynchronous invocation. The settings that it supports are `Maximum age of event` and `Retry attempts` as stated in [Lambda documentation for Configuring error handling for asynchronous invocation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-async-errors). To configure these settings, refer to the aws_lambda_function_event_invoke_config resource. + +## CloudWatch Logging and Permissions + +For more information about CloudWatch Logs for Lambda, see the [Lambda User Guide](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions-logs.html). + + +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as aws from "@pulumi/aws"; + +const config = new pulumi.Config(); +const lambdaFunctionName = config.get("lambdaFunctionName") || "lambda_function_name"; +// This is to optionally manage the CloudWatch Log Group for the Lambda Function. +// If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. +const example = new aws.cloudwatch.LogGroup("example", {retentionInDays: 14}); +const lambdaLoggingPolicyDocument = aws.iam.getPolicyDocument({ + statements: [{ + effect: "Allow", + actions: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + resources: ["arn:aws:logs:*:*:*"], + }], +}); +const lambdaLoggingPolicy = new aws.iam.Policy("lambdaLoggingPolicy", { + path: "/", + description: "IAM policy for logging from a lambda", + policy: lambdaLoggingPolicyDocument.then(lambdaLoggingPolicyDocument => lambdaLoggingPolicyDocument.json), +}); +const lambdaLogs = new aws.iam.RolePolicyAttachment("lambdaLogs", { + role: aws_iam_role.iam_for_lambda.name, + policyArn: lambdaLoggingPolicy.arn, +}); +const testLambda = new aws.lambda.Function("testLambda", {loggingConfig: { + logFormat: "Text", +}}, { + dependsOn: [ + lambdaLogs, + example, + ], +}); +``` +```python +import pulumi +import pulumi_aws as aws + +config = pulumi.Config() +lambda_function_name = config.get("lambdaFunctionName") +if lambda_function_name is None: + lambda_function_name = "lambda_function_name" +# This is to optionally manage the CloudWatch Log Group for the Lambda Function. +# If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. +example = aws.cloudwatch.LogGroup("example", retention_in_days=14) +lambda_logging_policy_document = aws.iam.get_policy_document(statements=[aws.iam.GetPolicyDocumentStatementArgs( + effect="Allow", + actions=[ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + resources=["arn:aws:logs:*:*:*"], +)]) +lambda_logging_policy = aws.iam.Policy("lambdaLoggingPolicy", + path="/", + description="IAM policy for logging from a lambda", + policy=lambda_logging_policy_document.json) +lambda_logs = aws.iam.RolePolicyAttachment("lambdaLogs", + role=aws_iam_role["iam_for_lambda"]["name"], + policy_arn=lambda_logging_policy.arn) +test_lambda = aws.lambda_.Function("testLambda", logging_config=aws.lambda_.FunctionLoggingConfigArgs( + log_format="Text", +), +opts=pulumi.ResourceOptions(depends_on=[ + lambda_logs, + example, + ])) +``` +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Aws = Pulumi.Aws; + +return await Deployment.RunAsync(() => +{ + var config = new Config(); + var lambdaFunctionName = config.Get("lambdaFunctionName") ?? "lambda_function_name"; + // This is to optionally manage the CloudWatch Log Group for the Lambda Function. + // If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. + var example = new Aws.CloudWatch.LogGroup("example", new() + { + RetentionInDays = 14, + }); + + var lambdaLoggingPolicyDocument = Aws.Iam.GetPolicyDocument.Invoke(new() + { + Statements = new[] + { + new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs + { + Effect = "Allow", + Actions = new[] + { + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + }, + Resources = new[] + { + "arn:aws:logs:*:*:*", + }, + }, + }, + }); + + var lambdaLoggingPolicy = new Aws.Iam.Policy("lambdaLoggingPolicy", new() + { + Path = "/", + Description = "IAM policy for logging from a lambda", + PolicyDocument = lambdaLoggingPolicyDocument.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json), + }); + + var lambdaLogs = new Aws.Iam.RolePolicyAttachment("lambdaLogs", new() + { + Role = aws_iam_role.Iam_for_lambda.Name, + PolicyArn = lambdaLoggingPolicy.Arn, + }); + + var testLambda = new Aws.Lambda.Function("testLambda", new() + { + LoggingConfig = new Aws.Lambda.Inputs.FunctionLoggingConfigArgs + { + LogFormat = "Text", + }, + }, new CustomResourceOptions + { + DependsOn = new[] + { + lambdaLogs, + example, + }, + }); + +}); +``` +```go +package main + +import ( + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/cloudwatch" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + cfg := config.New(ctx, "") + lambdaFunctionName := "lambda_function_name" + if param := cfg.Get("lambdaFunctionName"); param != "" { + lambdaFunctionName = param + } + // This is to optionally manage the CloudWatch Log Group for the Lambda Function. + // If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. + example, err := cloudwatch.NewLogGroup(ctx, "example", &cloudwatch.LogGroupArgs{ + RetentionInDays: pulumi.Int(14), + }) + if err != nil { + return err + } + lambdaLoggingPolicyDocument, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{ + Statements: []iam.GetPolicyDocumentStatement{ + { + Effect: pulumi.StringRef("Allow"), + Actions: []string{ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + }, + Resources: []string{ + "arn:aws:logs:*:*:*", + }, + }, + }, + }, nil) + if err != nil { + return err + } + lambdaLoggingPolicy, err := iam.NewPolicy(ctx, "lambdaLoggingPolicy", &iam.PolicyArgs{ + Path: pulumi.String("/"), + Description: pulumi.String("IAM policy for logging from a lambda"), + Policy: *pulumi.String(lambdaLoggingPolicyDocument.Json), + }) + if err != nil { + return err + } + lambdaLogs, err := iam.NewRolePolicyAttachment(ctx, "lambdaLogs", &iam.RolePolicyAttachmentArgs{ + Role: pulumi.Any(aws_iam_role.Iam_for_lambda.Name), + PolicyArn: lambdaLoggingPolicy.Arn, + }) + if err != nil { + return err + } + _, err = lambda.NewFunction(ctx, "testLambda", &lambda.FunctionArgs{ + LoggingConfig: &lambda.FunctionLoggingConfigArgs{ + LogFormat: pulumi.String("Text"), + }, + }, pulumi.DependsOn([]pulumi.Resource{ + lambdaLogs, + example, + })) + if err != nil { + return err + } + return nil + }) +} +``` +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.aws.cloudwatch.LogGroup; +import com.pulumi.aws.cloudwatch.LogGroupArgs; +import com.pulumi.aws.iam.IamFunctions; +import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs; +import com.pulumi.aws.iam.Policy; +import com.pulumi.aws.iam.PolicyArgs; +import com.pulumi.aws.iam.RolePolicyAttachment; +import com.pulumi.aws.iam.RolePolicyAttachmentArgs; +import com.pulumi.aws.lambda.Function; +import com.pulumi.aws.lambda.FunctionArgs; +import com.pulumi.aws.lambda.inputs.FunctionLoggingConfigArgs; +import com.pulumi.resources.CustomResourceOptions; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + final var config = ctx.config(); + final var lambdaFunctionName = config.get("lambdaFunctionName").orElse("lambda_function_name"); + var example = new LogGroup("example", LogGroupArgs.builder() + .retentionInDays(14) + .build()); + + final var lambdaLoggingPolicyDocument = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder() + .statements(GetPolicyDocumentStatementArgs.builder() + .effect("Allow") + .actions( + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents") + .resources("arn:aws:logs:*:*:*") + .build()) + .build()); + + var lambdaLoggingPolicy = new Policy("lambdaLoggingPolicy", PolicyArgs.builder() + .path("/") + .description("IAM policy for logging from a lambda") + .policy(lambdaLoggingPolicyDocument.applyValue(getPolicyDocumentResult -> getPolicyDocumentResult.json())) + .build()); + + var lambdaLogs = new RolePolicyAttachment("lambdaLogs", RolePolicyAttachmentArgs.builder() + .role(aws_iam_role.iam_for_lambda().name()) + .policyArn(lambdaLoggingPolicy.arn()) + .build()); + + var testLambda = new Function("testLambda", FunctionArgs.builder() + .loggingConfig(FunctionLoggingConfigArgs.builder() + .logFormat("Text") + .build()) + .build(), CustomResourceOptions.builder() + .dependsOn( + lambdaLogs, + example) + .build()); + + } +} +``` +```yaml +configuration: + lambdaFunctionName: + type: string + default: lambda_function_name +resources: + testLambda: + type: aws:lambda:Function + properties: + loggingConfig: + logFormat: Text + options: + dependson: + - ${lambdaLogs} + - ${example} + # This is to optionally manage the CloudWatch Log Group for the Lambda Function. + # If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. + example: + type: aws:cloudwatch:LogGroup + properties: + retentionInDays: 14 + lambdaLoggingPolicy: + type: aws:iam:Policy + properties: + path: / + description: IAM policy for logging from a lambda + policy: ${lambdaLoggingPolicyDocument.json} + lambdaLogs: + type: aws:iam:RolePolicyAttachment + properties: + role: ${aws_iam_role.iam_for_lambda.name} + policyArn: ${lambdaLoggingPolicy.arn} +variables: + lambdaLoggingPolicyDocument: + fn::invoke: + Function: aws:iam:getPolicyDocument + Arguments: + statements: + - effect: Allow + actions: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + resources: + - arn:aws:logs:*:*:* +``` + + +## Specifying the Deployment Package + +AWS Lambda expects source code to be provided as a deployment package whose structure varies depending on which `runtime` is in use. See [Runtimes](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime) for the valid values of `runtime`. The expected structure of the deployment package can be found in [the AWS Lambda documentation for each runtime](https://docs.aws.amazon.com/lambda/latest/dg/deployment-package-v2.html). + +Once you have created your deployment package you can specify it either directly as a local file (using the `filename` argument) or indirectly via Amazon S3 (using the `s3_bucket`, `s3_key` and `s3_object_version` arguments). When providing the deployment package via S3 it may be useful to use the `aws_s3_object` resource to upload it. + +For larger deployment packages it is recommended by Amazon to upload via S3, since the S3 API has better support for uploading large files efficiently. + +## Import + +Using `pulumi import`, import Lambda Functions using the `function_name`. For example: + +```sh +$ pulumi import aws:lambda/function:Function test_lambda my_test_lambda_function +``` diff --git a/pkg/tfgen/test_data/convertExamples/code_tagged_json_stays_in_description.md b/pkg/tfgen/test_data/convertExamples/code_tagged_json_stays_in_description.md new file mode 100644 index 000000000..c335a6b88 --- /dev/null +++ b/pkg/tfgen/test_data/convertExamples/code_tagged_json_stays_in_description.md @@ -0,0 +1,96 @@ +This is an example resource that has no code transformations but valid code blocks. It should render as-is. + +## Example Usage + +### Basic Example + +* section1.json + +```json +{ + "name": "section 1", + "rows": [ + { + "charts": [ + { + "name": "chart 1", + "sources": [ + { + "name": "source 1", + "query": "ts()", + "scatterPlotSource": "Y", + "querybuilderEnabled": false, + "sourceDescription": "" + } + ], + "units": "someunit", + "base": 0, + "noDefaultEvents": false, + "interpolatePoints": false, + "includeObsoleteMetrics": false, + "description": "This is chart 1, showing something", + "chartSettings": { + "type": "markdown-widget", + "max": 100, + "expectedDataSpacing": 120, + "windowing": "full", + "windowSize": 10, + "autoColumnTags": false, + "columnTags": "deprecated", + "tagMode": "all", + "numTags": 2, + "customTags": [ + "tag1", + "tag2" + ], + "groupBySource": true, + "y1Max": 100, + "y1Units": "units", + "y0ScaleSIBy1024": true, + "y1ScaleSIBy1024": true, + "y0UnitAutoscaling": true, + "y1UnitAutoscaling": true, + "fixedLegendEnabled": true, + "fixedLegendUseRawStats": true, + "fixedLegendPosition": "RIGHT", + "fixedLegendDisplayStats": [ + "stat1", + "stat2" + ], + "fixedLegendFilterSort": "TOP", + "fixedLegendFilterLimit": 1, + "fixedLegendFilterField": "CURRENT", + "plainMarkdownContent": "markdown content" + }, + "summarization": "MEAN" + } + ], + "heightFactor": 50 + } + ] +} +``` + +* parameters.json + +```json +{ + "param": { + "hideFromView": false, + "description": null, + "allowAll": null, + "tagKey": null, + "queryValue": null, + "dynamicFieldType": null, + "reverseDynSort": null, + "parameterType": "SIMPLE", + "label": "test", + "defaultValue": "Label", + "valuesToReadableStrings": { + "Label": "test" + }, + "selectedLabel": "Label", + "value": "test" + } +} +``` diff --git a/pkg/tfgen/test_data/convertExamples/code_tagged_json_stays_in_description_out.md b/pkg/tfgen/test_data/convertExamples/code_tagged_json_stays_in_description_out.md new file mode 100644 index 000000000..c335a6b88 --- /dev/null +++ b/pkg/tfgen/test_data/convertExamples/code_tagged_json_stays_in_description_out.md @@ -0,0 +1,96 @@ +This is an example resource that has no code transformations but valid code blocks. It should render as-is. + +## Example Usage + +### Basic Example + +* section1.json + +```json +{ + "name": "section 1", + "rows": [ + { + "charts": [ + { + "name": "chart 1", + "sources": [ + { + "name": "source 1", + "query": "ts()", + "scatterPlotSource": "Y", + "querybuilderEnabled": false, + "sourceDescription": "" + } + ], + "units": "someunit", + "base": 0, + "noDefaultEvents": false, + "interpolatePoints": false, + "includeObsoleteMetrics": false, + "description": "This is chart 1, showing something", + "chartSettings": { + "type": "markdown-widget", + "max": 100, + "expectedDataSpacing": 120, + "windowing": "full", + "windowSize": 10, + "autoColumnTags": false, + "columnTags": "deprecated", + "tagMode": "all", + "numTags": 2, + "customTags": [ + "tag1", + "tag2" + ], + "groupBySource": true, + "y1Max": 100, + "y1Units": "units", + "y0ScaleSIBy1024": true, + "y1ScaleSIBy1024": true, + "y0UnitAutoscaling": true, + "y1UnitAutoscaling": true, + "fixedLegendEnabled": true, + "fixedLegendUseRawStats": true, + "fixedLegendPosition": "RIGHT", + "fixedLegendDisplayStats": [ + "stat1", + "stat2" + ], + "fixedLegendFilterSort": "TOP", + "fixedLegendFilterLimit": 1, + "fixedLegendFilterField": "CURRENT", + "plainMarkdownContent": "markdown content" + }, + "summarization": "MEAN" + } + ], + "heightFactor": 50 + } + ] +} +``` + +* parameters.json + +```json +{ + "param": { + "hideFromView": false, + "description": null, + "allowAll": null, + "tagKey": null, + "queryValue": null, + "dynamicFieldType": null, + "reverseDynSort": null, + "parameterType": "SIMPLE", + "label": "test", + "defaultValue": "Label", + "valuesToReadableStrings": { + "Label": "test" + }, + "selectedLabel": "Label", + "value": "test" + } +} +``` diff --git a/pkg/tfgen/test_data/convertExamples/inline_fences_are_preserved.md b/pkg/tfgen/test_data/convertExamples/inline_fences_are_preserved.md new file mode 100644 index 000000000..d1beb1c9d --- /dev/null +++ b/pkg/tfgen/test_data/convertExamples/inline_fences_are_preserved.md @@ -0,0 +1,5 @@ +This is an example resource that has no code transformations or headers but valid inline fences. It should render as-is. + +```this is actually just some resource name``` + +Yay testing! \ No newline at end of file diff --git a/pkg/tfgen/test_data/convertExamples/inline_fences_are_preserved_out.md b/pkg/tfgen/test_data/convertExamples/inline_fences_are_preserved_out.md new file mode 100644 index 000000000..d1beb1c9d --- /dev/null +++ b/pkg/tfgen/test_data/convertExamples/inline_fences_are_preserved_out.md @@ -0,0 +1,5 @@ +This is an example resource that has no code transformations or headers but valid inline fences. It should render as-is. + +```this is actually just some resource name``` + +Yay testing! \ No newline at end of file diff --git a/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json.md b/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json.md index 74a92dd3a..3635ae9ff 100644 --- a/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json.md +++ b/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json.md @@ -270,6 +270,6 @@ The sample files are listed below. Dashboard JSON can be imported by using the `id`, e.g.: -```sh +```sh $ pulumi import wavefront:index/dashboardJson:DashboardJson dashboard_json tftestimport -``` +``` diff --git a/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json_out.md b/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json_out.md index f36024c2a..a89ab7e40 100644 --- a/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json_out.md +++ b/pkg/tfgen/test_data/convertExamples/wavefront_dashboard_json_out.md @@ -1,9 +1,8 @@ Provides a Wavefront Dashboard JSON resource. This allows dashboards to be created, updated, and deleted. -{{% examples %}} ## Example Usage -{{% example %}} + ```typescript import * as pulumi from "@pulumi/pulumi"; import * as wavefront from "@pulumi/wavefront"; @@ -806,18 +805,16 @@ resources: } } ``` + * *Note: ** If there are dynamic variables in the Wavefront dashboard json, then these variables must be present in a separate file as mentioned in the section below. -{{% /example %}} -{{% /examples %}} ## Import Dashboard JSON can be imported by using the `id`, e.g.: ```sh - $ pulumi import wavefront:index/dashboardJson:DashboardJson dashboard_json tftestimport +$ pulumi import wavefront:index/dashboardJson:DashboardJson dashboard_json tftestimport ``` - \ No newline at end of file diff --git a/pkg/tfgen/test_data/minimuxed-schema.json b/pkg/tfgen/test_data/minimuxed-schema.json index 311a4371c..e7a4a18c1 100644 --- a/pkg/tfgen/test_data/minimuxed-schema.json +++ b/pkg/tfgen/test_data/minimuxed-schema.json @@ -34,10 +34,10 @@ "type": "string" } }, - "required":[ + "type": "object", + "required": [ "value" - ], - "type": "object" + ] } }, "provider": { diff --git a/pkg/tfgen/test_data/minirandom-schema-csharp.json b/pkg/tfgen/test_data/minirandom-schema-csharp.json index 5dc2608b5..07c6514d5 100644 --- a/pkg/tfgen/test_data/minirandom-schema-csharp.json +++ b/pkg/tfgen/test_data/minirandom-schema-csharp.json @@ -56,7 +56,7 @@ "description": "A custom seed to always produce the same value.\n", "language": { "csharp": { - "name": "CSharpSeed" + "name": "CSharpSeed" } } } @@ -66,6 +66,11 @@ "min", "result" ], + "language": { + "csharp": { + "name": "CSharpRandomInteger" + } + }, "inputProperties": { "keepers": { "type": "object", @@ -88,12 +93,12 @@ "seed": { "type": "string", "description": "A custom seed to always produce the same value.\n", - "willReplaceOnChanges": true, "language": { "csharp": { - "name": "CSharpSeed" + "name": "CSharpSeed" } - } + }, + "willReplaceOnChanges": true } }, "requiredInputs": [ @@ -128,20 +133,15 @@ "seed": { "type": "string", "description": "A custom seed to always produce the same value.\n", - "willReplaceOnChanges": true, "language": { "csharp": { - "name": "CSharpSeed" + "name": "CSharpSeed" } - } + }, + "willReplaceOnChanges": true } }, "type": "object" - }, - "language": { - "csharp": { - "name": "CSharpRandomInteger" - } } } } diff --git a/pkg/tfgen/test_data/nested-descriptions-schema.json b/pkg/tfgen/test_data/nested-descriptions-schema.json index b9fa51117..d8d5ea17f 100644 --- a/pkg/tfgen/test_data/nested-descriptions-schema.json +++ b/pkg/tfgen/test_data/nested-descriptions-schema.json @@ -58,7 +58,7 @@ "type": "string" }, "translateField": { - "type": "string", + "type": "string", "description": "When cloudflare.Ruleset is mentioned, it should be translated.\n" } }, diff --git a/pkg/tfgen/test_data/parse-imports/accessanalyzer-expected.md b/pkg/tfgen/test_data/parse-imports/accessanalyzer-expected.md index f3621317d..d1e309a20 100644 --- a/pkg/tfgen/test_data/parse-imports/accessanalyzer-expected.md +++ b/pkg/tfgen/test_data/parse-imports/accessanalyzer-expected.md @@ -2,6 +2,6 @@ Using `pulumi import`, import Access Analyzer Analyzers using the `analyzer_name`. For example: -```sh +```sh $ pulumi import aws:accessanalyzer/analyzer:Analyzer example example -``` +``` diff --git a/pkg/tfgen/test_data/parse-imports/gameliftconfig-expected.md b/pkg/tfgen/test_data/parse-imports/gameliftconfig-expected.md index 7101abc6d..2c8758d1d 100644 --- a/pkg/tfgen/test_data/parse-imports/gameliftconfig-expected.md +++ b/pkg/tfgen/test_data/parse-imports/gameliftconfig-expected.md @@ -2,6 +2,6 @@ GameLift Matchmaking Configurations can be imported using the ID, e.g., -```sh +```sh $ pulumi import aws:gamelift/matchmakingConfiguration:MatchmakingConfiguration example -``` +``` diff --git a/pkg/tfgen/test_data/parse-imports/lambdalayer-expected.md b/pkg/tfgen/test_data/parse-imports/lambdalayer-expected.md index af0edf93c..01e7171f6 100644 --- a/pkg/tfgen/test_data/parse-imports/lambdalayer-expected.md +++ b/pkg/tfgen/test_data/parse-imports/lambdalayer-expected.md @@ -2,6 +2,6 @@ Using `pulumi import`, import Lambda Layers using `arn`. For example: -```sh +```sh $ pulumi import aws:lambda/layerVersion:LayerVersion test_layer arn:aws:lambda:_REGION_:_ACCOUNT_ID_:layer:_LAYER_NAME_:_LAYER_VERSION_ -``` +``` diff --git a/pkg/tfgen/test_data/parse-inner-docs/aws_lambda_function_description.md b/pkg/tfgen/test_data/parse-inner-docs/aws_lambda_function_description.md new file mode 100644 index 000000000..655c97e9a --- /dev/null +++ b/pkg/tfgen/test_data/parse-inner-docs/aws_lambda_function_description.md @@ -0,0 +1,250 @@ +Provides a Lambda Function resource. Lambda allows you to trigger execution of code in response to events in AWS, enabling serverless backend solutions. The Lambda Function itself includes source code and runtime configuration. + +For information about Lambda and how to use it, see [What is AWS Lambda?][1] + +For a detailed example of setting up Lambda and API Gateway, see [Serverless Applications with AWS Lambda and API Gateway.][11] + +~> **NOTE:** Due to [AWS Lambda improved VPC networking changes that began deploying in September 2019](https://aws.amazon.com/blogs/compute/announcing-improved-vpc-networking-for-aws-lambda-functions/), EC2 subnets and security groups associated with Lambda Functions can take up to 45 minutes to successfully delete. + +~> **NOTE:** If you get a `KMSAccessDeniedException: Lambda was unable to decrypt the environment variables because KMS access was denied` error when invoking an `aws_lambda_function` with environment variables, the IAM role associated with the function may have been deleted and recreated _after_ the function was created. You can fix the problem two ways: 1) updating the function's role to another role and then updating it back again to the recreated role, or 2) by using Terraform to `taint` the function and `apply` your configuration again to recreate the function. (When you create a function, Lambda grants permissions on the KMS key to the function's IAM role. If the IAM role is recreated, the grant is no longer valid. Changing the function's role or recreating the function causes Lambda to update the grant.) + +-> To give an external source (like an EventBridge Rule, SNS, or S3) permission to access the Lambda function, use the `aws_lambda_permission` resource. See [Lambda Permission Model][4] for more details. On the other hand, the `role` argument of this resource is the function's execution role for identity and access to AWS services and resources. + +## Example Usage + +### Basic Example + +```terraform +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "archive_file" "lambda" { + type = "zip" + source_file = "lambda.js" + output_path = "lambda_function_payload.zip" +} + +resource "aws_lambda_function" "test_lambda" { + # If the file is not in the current working directory you will need to include a + # path.module in the filename. + filename = "lambda_function_payload.zip" + function_name = "lambda_function_name" + role = aws_iam_role.iam_for_lambda.arn + handler = "index.test" + + source_code_hash = data.archive_file.lambda.output_base64sha256 + + runtime = "nodejs18.x" + + environment { + variables = { + foo = "bar" + } + } +} +``` + +### Lambda Layers + +```terraform +resource "aws_lambda_layer_version" "example" { + # ... other configuration ... +} + +resource "aws_lambda_function" "example" { + # ... other configuration ... + layers = [aws_lambda_layer_version.example.arn] +} +``` + +### Lambda Ephemeral Storage + +Lambda Function Ephemeral Storage(`/tmp`) allows you to configure the storage upto `10` GB. The default value set to `512` MB. + +```terraform +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_lambda_function" "test_lambda" { + filename = "lambda_function_payload.zip" + function_name = "lambda_function_name" + role = aws_iam_role.iam_for_lambda.arn + handler = "index.test" + runtime = "nodejs18.x" + + ephemeral_storage { + size = 10240 # Min 512 MB and the Max 10240 MB + } +} +``` + +### Lambda File Systems + +Lambda File Systems allow you to connect an Amazon Elastic File System (EFS) file system to a Lambda function to share data across function invocations, access existing data including large files, and save function state. + +```terraform +# A lambda function connected to an EFS file system +resource "aws_lambda_function" "example" { + # ... other configuration ... + + file_system_config { + # EFS file system access point ARN + arn = aws_efs_access_point.access_point_for_lambda.arn + + # Local mount path inside the lambda function. Must start with '/mnt/'. + local_mount_path = "/mnt/efs" + } + + vpc_config { + # Every subnet should be able to reach an EFS mount target in the same Availability Zone. Cross-AZ mounts are not permitted. + subnet_ids = [aws_subnet.subnet_for_lambda.id] + security_group_ids = [aws_security_group.sg_for_lambda.id] + } + + # Explicitly declare dependency on EFS mount target. + # When creating or updating Lambda functions, mount target must be in 'available' lifecycle state. + depends_on = [aws_efs_mount_target.alpha] +} + +# EFS file system +resource "aws_efs_file_system" "efs_for_lambda" { + tags = { + Name = "efs_for_lambda" + } +} + +# Mount target connects the file system to the subnet +resource "aws_efs_mount_target" "alpha" { + file_system_id = aws_efs_file_system.efs_for_lambda.id + subnet_id = aws_subnet.subnet_for_lambda.id + security_groups = [aws_security_group.sg_for_lambda.id] +} + +# EFS access point used by lambda file system +resource "aws_efs_access_point" "access_point_for_lambda" { + file_system_id = aws_efs_file_system.efs_for_lambda.id + + root_directory { + path = "/lambda" + creation_info { + owner_gid = 1000 + owner_uid = 1000 + permissions = "777" + } + } + + posix_user { + gid = 1000 + uid = 1000 + } +} +``` + +### Lambda retries + +Lambda Functions allow you to configure error handling for asynchronous invocation. The settings that it supports are `Maximum age of event` and `Retry attempts` as stated in [Lambda documentation for Configuring error handling for asynchronous invocation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-async-errors). To configure these settings, refer to the aws_lambda_function_event_invoke_config resource. + +## CloudWatch Logging and Permissions + +For more information about CloudWatch Logs for Lambda, see the [Lambda User Guide](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-functions-logs.html). + +```terraform +variable "lambda_function_name" { + default = "lambda_function_name" +} + +resource "aws_lambda_function" "test_lambda" { + function_name = var.lambda_function_name + + # Advanced logging controls (optional) + logging_config { + log_format = "Text" + } + + # ... other configuration ... + depends_on = [ + aws_iam_role_policy_attachment.lambda_logs, + aws_cloudwatch_log_group.example, + ] +} + +# This is to optionally manage the CloudWatch Log Group for the Lambda Function. +# If skipping this resource configuration, also add "logs:CreateLogGroup" to the IAM policy below. +resource "aws_cloudwatch_log_group" "example" { + name = "/aws/lambda/${var.lambda_function_name}" + retention_in_days = 14 +} + +# See also the following AWS managed policy: AWSLambdaBasicExecutionRole +data "aws_iam_policy_document" "lambda_logging" { + statement { + effect = "Allow" + + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + + resources = ["arn:aws:logs:*:*:*"] + } +} + +resource "aws_iam_policy" "lambda_logging" { + name = "lambda_logging" + path = "/" + description = "IAM policy for logging from a lambda" + policy = data.aws_iam_policy_document.lambda_logging.json +} + +resource "aws_iam_role_policy_attachment" "lambda_logs" { + role = aws_iam_role.iam_for_lambda.name + policy_arn = aws_iam_policy.lambda_logging.arn +} +``` + +## Specifying the Deployment Package + +AWS Lambda expects source code to be provided as a deployment package whose structure varies depending on which `runtime` is in use. See [Runtimes](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime) for the valid values of `runtime`. The expected structure of the deployment package can be found in [the AWS Lambda documentation for each runtime](https://docs.aws.amazon.com/lambda/latest/dg/deployment-package-v2.html). + +Once you have created your deployment package you can specify it either directly as a local file (using the `filename` argument) or indirectly via Amazon S3 (using the `s3_bucket`, `s3_key` and `s3_object_version` arguments). When providing the deployment package via S3 it may be useful to use the `aws_s3_object` resource to upload it. + +For larger deployment packages it is recommended by Amazon to upload via S3, since the S3 API has better support for uploading large files efficiently. + +## Import + +Using `pulumi import`, import Lambda Functions using the `function_name`. For example: + +```sh +$ pulumi import aws:lambda/function:Function test_lambda my_test_lambda_function +``` diff --git a/pkg/tfgen/test_data/parse-inner-docs/starts-with-code-block.md b/pkg/tfgen/test_data/parse-inner-docs/starts-with-code-block.md new file mode 100644 index 000000000..bf74fbd2b --- /dev/null +++ b/pkg/tfgen/test_data/parse-inner-docs/starts-with-code-block.md @@ -0,0 +1,5 @@ +``` +This is a code block. +We should parse it. +``` +This doc has no headers. \ No newline at end of file diff --git a/pkg/tfgen/test_data/parse-inner-docs/starts-with-h2.md b/pkg/tfgen/test_data/parse-inner-docs/starts-with-h2.md new file mode 100644 index 000000000..b75b862a4 --- /dev/null +++ b/pkg/tfgen/test_data/parse-inner-docs/starts-with-h2.md @@ -0,0 +1,7 @@ +## This document starts with an H2 header. + +We want to make sure this header is detected. + +``` +#### this should never be counted as a header. +``` diff --git a/pkg/tfgen/test_data/parse-inner-docs/starts-with-h3.md b/pkg/tfgen/test_data/parse-inner-docs/starts-with-h3.md new file mode 100644 index 000000000..453d8315c --- /dev/null +++ b/pkg/tfgen/test_data/parse-inner-docs/starts-with-h3.md @@ -0,0 +1,7 @@ +### This document starts with an H3 header. + +We want to make sure this header is detected. + +``` +this is some code +```