diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2833232 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text eol=lf +*.png binary +*.gif binary diff --git a/.github/ISSUE_TEMPLATE/00-bug.yml b/.github/ISSUE_TEMPLATE/00-bug.yml new file mode 100644 index 0000000..51bf101 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/00-bug.yml @@ -0,0 +1,56 @@ +name: "🐛 Report a documentation issue" +description: >- + Report an issue with current documentation. +labels: + - bug + - needs-triage +body: + - type: markdown + attributes: + value: >- + Use this template to report issues with documentation. This can include typos, technical + and factual errors, grammar, spelling, formatting, presentation, etc. + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: >- + These steps are required. After you've completed each step, check the box for it before + moving on. + options: + - label: >- + **Existing Issue:** Search the existing issues for this repository. If there is an + issue that fits your needs do not file a new one. Subscribe, react, or comment on that + issue instead. + required: true + - label: >- + **Descriptive Title:** Write the title for this issue as a short synopsis. If possible, + provide context. + required: true + - type: textarea + id: summary + validations: + required: true + attributes: + label: Summary + description: >- + Write a clear and concise description of the problem. + - type: textarea + id: details + validations: + required: false + attributes: + label: Details + description: >- + If possible, please provide extended details that will add context and help the team update + the documentation. Additional details may not be useful for typos, grammar, formatting, etc. + For technical or factual errors, please include code snippets and output to show how the + documentation or implementation is incorrect. + - type: textarea + id: suggestion + validations: + required: false + attributes: + label: Suggested Fix + description: >- + If you have an idea for how to fix the problem you're identifying, include it here. diff --git a/.github/ISSUE_TEMPLATE/01-new-tutorial.yml b/.github/ISSUE_TEMPLATE/01-new-tutorial.yml new file mode 100644 index 0000000..12dae73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-new-tutorial.yml @@ -0,0 +1,72 @@ +name: "📝 Request a new tutorial" +description: >- + Request a new tutorial for the DSC Samples. If you want to request a new language implementation + for an existing tutorial, use the "Request a new implementation" template instead. +labels: + - request-tutorial + - needs-triage +body: + - type: markdown + attributes: + value: >- + Use this template to request or suggest a new tutorial for the DSC samples. Tutorials are + always accompanied by sample code. DSC Resource tutorials have multiple language + implementations for a shared set of learning goals. You can find more information about + contributing tutorials in the + [contributing guide](https://powershell.github.io/DSC-Samples/contributing/tutorials/). + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: >- + These steps are required. After you've completed each step, check the box for it before + moving on. + options: + - label: >- + **Existing Issue:** Search the existing issues for this repository. If there is an + issue that fits your needs do not file a new one. Subscribe, react, or comment on that + issue instead. + required: true + - label: >- + **Descriptive Title:** Write the title for this issue as a short synopsis. If possible, + provide context. For example, "Tutorial for error handling in a DSC Resource" instead + of "Handle errors" + required: true + - type: dropdown + id: kind + attributes: + label: Tutorial Kind + description: >- + Generic tutorials only have a single implementation. These tutorials are primarily for + authoring DSC Configuration documents and showing how to use DSC itself. Resource tutorials + may be implemented in any number of languages. Resource tutorial implementations always + share the same learning goals and their sample code implements the same specification. + multiple: false + options: + - Generic + - Resource + - type: textarea + id: summary + validations: + required: true + attributes: + label: Summary + description: >- + Write a clear and concise description of the proposed tutorial. Why is it needed? What gap + will it fill? Who is the intended user? What scenarios will it address? + placeholder: | + Try formulating the need for this tutorial as a user story. + + > As a , I want a tutorial that . + - type: textarea + id: details + validations: + required: false + attributes: + label: Details + description: >- + If possible, please provide extended details that will add context and help the team + understand the proposed tutorial. If you're requesting a resource tutorial and are + interested in contributing an implementation, please indicate which language you'd like to + author the implementation in. diff --git a/.github/ISSUE_TEMPLATE/02-new-implementation.yml b/.github/ISSUE_TEMPLATE/02-new-implementation.yml new file mode 100644 index 0000000..deb6877 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-new-implementation.yml @@ -0,0 +1,59 @@ +name: "📦 Request a new implementation" +description: >- + Request a new language implementation for an existing tutorial. If you want to request an + entirely new tutorial, use the "Request a new tutorial" template instead. +labels: + - request-implementation + - needs-triage +body: + - type: markdown + attributes: + value: >- + Use this template to request a new language implementation for an existing resource + tutorial. DSC Resource tutorials have multiple language implementations for a shared set of + learning goals. You can find more information about contributing tutorials in the + [contributing guide](https://powershell.github.io/DSC-Samples/contributing/tutorials/). + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + description: >- + These steps are required. After you've completed each step, check the box for it before + moving on. + options: + - label: >- + **Existing Issue:** Search the existing issues for this repository. If there is an + issue that fits your needs do not file a new one. Subscribe, react, or comment on that + issue instead. + required: true + - label: >- + **Descriptive Title:** Write the title for this issue as a short synopsis. If possible, + provide context. For example, "Tutorial for error handling in a DSC Resource" instead + of "Handle errors" + required: true + - type: dropdown + id: tutorial + attributes: + label: Tutorial + description: >- + Specify the name of the tutorial you're requesting a new language implementation for. + multiple: false + options: + - Write your first DSC Resource + - type: input + id: language + attributes: + label: Implementation language + description: >- + Specify the programming language you want the new implementation to use. The team has + experience with C#, Go, and Rust, but you can request the implementation in any language. + - type: textarea + id: details + validations: + required: false + attributes: + label: Details + description: >- + If possible, please provide extended details that will add context and help the team + with the implementation. If you're willing to contribute the implementation, please + include that information. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57b1342 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +Projects/Modules/Documentarian*/[0-9]*.[0-9]*.[0-9]*/ +# Source/Styles/* +# !Source/Styles/PowerShell-Docs/ +# !Source/Styles/Vocab/ +.vscode/styles +PackagedStyles +.vale + +# Hugo ignores +Site/public +Site/assets/vale +Projects/Hugo/**/public +**/resources/_gen +**/.hugo_build.lock +**/node_modules + +# Test ignores +Projects/Modules/**/*.Results.xml diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 0000000..ac5ff02 --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,35 @@ +# Rule definitions live in .markdownlint.json + +# Include a custom rule package +# customRules: +# - markdownlint-rule-titlecase + +# Fix any fixable errors +fix: true + +# Define a custom front matter pattern +# frontMatter: (^---\s*$[^]*?^---\s*$)(\r\n|\r|\n|$) + +# Define glob expressions to use (only valid at root) +# globs: +# - "!*bout.md" + +# Define glob expressions to ignore +ignores: + - .vscode + - assets + - tests + - tools + +# Use a plugin to recognize math +# markdownItPlugins: +# - - "@iktakahiro/markdown-it-katex" + +# Disable inline config comments +noInlineConfig: false + +# Disable progress on stdout (only valid at root) +noProgress: true +# Use a specific formatter (only valid at root) +# outputFormatters: +# - [markdownlint-cli2-formatter-default] diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..33420e2 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,120 @@ +{ + "blanks-around-fences": true, + "blanks-around-headers": true, + "blanks-around-lists": true, + "code-block-style": { + "style": "fenced" + }, + "code-fence-style": { + "style": "backtick" + }, + "commands-show-output": true, + "emphasis-style": { + "style": "underscore" + }, + "fenced-code-language": false, + "first-line-h1": { + "front_matter_title": "^\\s*title\\s*[:=]" + }, + "header-increment": true, + "header-start-left": true, + "header-style": { + "style": "atx" + }, + "hr-style": { + "style": "---" + }, + "line-length": { + "code_block_line_length": 90, + "code_blocks": true, + "heading_line_length": 100, + "headings": true, + "line_length": 100, + "stern": true, + "tables": false + }, + "list-indent": true, + "list-marker-space": true, + "no-alt-text": true, + "no-bare-urls": true, + "no-blanks-blockquote": true, + "no-duplicate-header": { + "siblings_only": true + }, + "no-emphasis-as-header": true, + "no-empty-links": true, + "no-hard-tabs": true, + "no-inline-html": { + "allowed_elements": [ + "a", + "br", + "code", + "kbd", + "li", + "properties", + "sup", + "tags", + "ul" + ] + }, + "no-missing-space-atx": true, + "no-missing-space-closed-atx": true, + "no-multiple-blanks": true, + "no-multiple-space-atx": true, + "no-multiple-space-blockquote": true, + "no-multiple-space-closed-atx": true, + "no-reversed-links": true, + "no-space-in-code": true, + "no-space-in-emphasis": true, + "no-space-in-links": true, + "no-trailing-punctuation": { + "punctuation": ".,;:!。,;:!?" + }, + "no-trailing-spaces": { + "br_spaces": 2, + "strict": true + }, + "ol-prefix": { + "style": "one" + }, + "proper-names": { + "code_blocks": false, + "names": [ + "PowerShell", + "IntelliSense", + "Authenticode", + "CentOS", + "Contoso", + "CoreOS", + "Debian", + "Ubuntu", + "openSUSE", + "RHEL", + "JavaScript", + ".NET", + "NuGet", + "VS Code", + "Newtonsoft" + ] + }, + "required-headers": false, + "single-h1": { + "front_matter_title": "", + "level": 1 + }, + "single-trailing-newline": true, + "strong-style": { + "style": "asterisk" + }, + "ul-indent": { + "indent": 2, + "start_indented": false + }, + "ul-start-left": true, + "ul-style": { + "style": "dash" + }, + "link-fragments": true, + "reference-links-images": true, + "link-image-reference-definitions": true +} diff --git a/README.md b/README.md index 64ae494..5fcc2ba 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# DSC-Samples \ No newline at end of file +# DSC-Samples + +This repository includes code samples for DSC v3. + + \ No newline at end of file diff --git a/_site/archetypes/default.md b/_site/archetypes/default.md new file mode 100644 index 0000000..00e77bd --- /dev/null +++ b/_site/archetypes/default.md @@ -0,0 +1,6 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + diff --git a/_site/data/_params/platen/display.yaml b/_site/data/_params/platen/display.yaml new file mode 100644 index 0000000..ab93e1e --- /dev/null +++ b/_site/data/_params/platen/display.yaml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://platen.io/modules/platen/config/site/display/settings/schema.json +table_of_contents: + minimum_level: 1 + maximum_level: 6 + use_legacy: false +date_format: "January 2, 2006" +menu: + root_section: / + languages_icon: + use_legacy: false + theme_options: + enabled: true + mode_selection: + enabled: true +mobile: + menu_control: + use_legacy: false + toc_control: + use_legacy: false +footer: + last_edited_on: + use_legacy: false + edit_this_page: + use_legacy: false +header: + title_as_heading: true \ No newline at end of file diff --git a/_site/data/_params/platen/features.yaml b/_site/data/_params/platen/features.yaml new file mode 100644 index 0000000..ba5fa44 --- /dev/null +++ b/_site/data/_params/platen/features.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://platen.io/modules/platen/config/site/config/schema.json#/properties/features +shoelace: + always_load: true + icons: + always_load_libraries: + - boxicons +search: + use_legacy: false + +dscs: + enabled: true + partials: + title: dscs/feature/title \ No newline at end of file diff --git a/_site/data/_params/platen/markup.yaml b/_site/data/_params/platen/markup.yaml new file mode 100644 index 0000000..8111d28 --- /dev/null +++ b/_site/data/_params/platen/markup.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://platen.io/modules/platen/config/site/config/schema.json#/properties/markup +details: + use_legacy: false + warn_on_legacy: false +buttons: + use_legacy: false + warn_on_legacy: false +tabs: + use_legacy: false + warn_on_legacy: false diff --git a/_site/data/_params/platen/repository.yaml b/_site/data/_params/platen/repository.yaml new file mode 100644 index 0000000..4ce1677 --- /dev/null +++ b/_site/data/_params/platen/repository.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://platen.io/modules/platen/config/site/repository/settings/schema.json +url: https://github.com/PowerShell/DSC-Samples +commit_path: commit +edit_path: edit/main +content_root: docs +mounted_paths: + - source: go/resources/first/docs + target: tutorials/first-resource/go + - source: go/resources/first/docs + target: languages/go/first-resource \ No newline at end of file diff --git a/_site/go.mod b/_site/go.mod new file mode 100644 index 0000000..b4be6be --- /dev/null +++ b/_site/go.mod @@ -0,0 +1,7 @@ +module github.com/PowerShell/DSC-Samples/_site + +go 1.19 + +require ( + github.com/platenio/platen/modules/platen v0.0.0-20230729184903-3451ed7a46af // indirect +) diff --git a/_site/go.sum b/_site/go.sum new file mode 100644 index 0000000..0769d9a --- /dev/null +++ b/_site/go.sum @@ -0,0 +1,2 @@ +github.com/platenio/platen/modules/platen v0.0.0-20230729184903-3451ed7a46af h1:lJl3fQ9BgxG0G0KqW8dcg0u9oDwt4NUfi2PTYYuHS1w= +github.com/platenio/platen/modules/platen v0.0.0-20230729184903-3451ed7a46af/go.mod h1:7pfizXCKb4vonp6Og/3zkoy09YYsHip9/hXiT/pc3IM= diff --git a/_site/hugo.yaml b/_site/hugo.yaml new file mode 100644 index 0000000..91dff77 --- /dev/null +++ b/_site/hugo.yaml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://platen.io/schemas/config/schema.json +# Update to your own settings for URL/title +baseURL: https://powershell.github.io/DSC-Samples +title: DSC Samples +contentDir: ../docs +themesDir: ../samples + +menu: + after: + - name: DSC Documentation + url: https://learn.microsoft.com/powershell/dsc/overview?view=dsc-3.0 + pre: + weight: 10 + - name: Samples Source + url: https://github.com/PowerShell/DSC-Samples + pre: + weight: 20 + +params: + description: Sample implementations for Desired State Configuration v3. + +markup: + defaultMarkdownHandler: goldmark + goldmark: + renderer: + unsafe: true + parser: + wrapStandAloneImageWithinParagraph: false + attribute: + block: true + highlight: + style: witchhazel + +module: + replacements: | + github.com/PowerShell/DSC-Samples/go/resources/first -> go/resources/first + imports: + - path: github.com/platenio/platen/modules/platen + - path: github.com/PowerShell/DSC-Samples/go/resources/first + mounts: + - source: docs + target: content/tutorials/first-resource/go + - source: docs + target: content/languages/go/first-resource diff --git a/_site/layouts/partials/dscs/feature/title.html b/_site/layouts/partials/dscs/feature/title.html new file mode 100644 index 0000000..f899f19 --- /dev/null +++ b/_site/layouts/partials/dscs/feature/title.html @@ -0,0 +1,40 @@ +{{/* Copyright (c) Microsoft Corporation. */}} +{{/* Licensed under the MIT License. */}} + +{{- $params := . -}} +{{- $context := . -}} +{{- $title := "" -}} + +{{- $isTarget := false -}} +{{- $info := dict -}} + +{{- if reflect.IsMap $params -}} + {{- $context = $params.Context -}} + {{- with $Options := $context.Params.dscs -}} + {{- $InTutorials := hasPrefix $context.RelPermalink "/DSC-Samples/tutorials/" -}} + {{- $InLanguages := hasPrefix $context.RelPermalink "/DSC-Samples/languages/" -}} + {{- $menuTitle := "" -}} + + {{- if and $InTutorials (isset $Options "tutorials_title") -}} + {{- $menuTitle = $Options.tutorials_title | default "" -}} + {{- else if and $InLanguages (isset $Options "languages_title") -}} + {{- $menuTitle = $Options.languages_title | default "" -}} + {{- else if isset $Options "menu_title" -}} + {{- $menuTitle = $Options.menu_title | default "" -}} + {{- end -}} + + {{- if and $params.ForMenu (ne $menuTitle "") -}} + {{- $title = $menuTitle -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{- with $info -}} + {{- warnf "Got title '%s' for '%s' with params: %s" + $title + $context.RelPermalink + ($info | jsonify (dict "indent" " " "noHTMLEscape" true)) + -}} +{{- end -}} + +{{- return $title -}} \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/app/.goreleaser.yaml b/app/.goreleaser.yaml new file mode 100644 index 0000000..9930dd9 --- /dev/null +++ b/app/.goreleaser.yaml @@ -0,0 +1,51 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +project_name: tstoy +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + # - go generate ./... +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + ldflags: + - -s -w + - -X github.com/PowerShell/DSC-Samples/app/cmd.version={{.Version}} + - -X github.com/PowerShell/DSC-Samples/app/cmd.commit={{.ShortCommit}} + - -X github.com/PowerShell/DSC-Samples/app/cmd.date={{.CommitDate}} + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..740d563 --- /dev/null +++ b/app/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2023 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..9e5722e --- /dev/null +++ b/app/README.md @@ -0,0 +1,34 @@ +# The `tstoy` example app + +This folder has the source code for a fictional application, `tstoy`. It's intended to serve as a +functional mockup of a real application a user might want to configure. + +It has two configuration options: + +1. Should the application update automatically? +1. How frequently should the application check for updates? + +These values are represented by keys in configuration files. The default configuration is: + +```json +{ + "updates": { + "automatic": false, + "checkFrequency": 90 + } +} +``` + +The application is implemented so that it can merge configuration from: + +1. The default configuration built into the code. +1. A machine-scope configuration file. +1. A user-scope configuration file. +1. Environment variables with the prefix `TSTOY_`. +1. Explicit flags passed to the application. + +DSC Resources targeting this application should be able to ensure: + +- Whether a configuration file in a specific scope exists. +- Whether a configuration file defines that the application should automatically update. +- The frequency of the automatic updates, if they're enabled. diff --git a/app/build.ps1 b/app/build.ps1 new file mode 100644 index 0000000..74adb47 --- /dev/null +++ b/app/build.ps1 @@ -0,0 +1,54 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param ( + [Parameter()] + [ValidateSet('build', 'package', 'test')] + [string]$Target = 'build', + [switch]$Initialize, + [switch]$AddToPath +) + +function Build-Project { + [cmdletbinding()] + [OutputType([System.Management.Automation.ApplicationInfo])] + param( + [switch]$All + ) + if ($All) { + goreleaser release --skip-publish --skip-announce --skip-validate --clean --release-notes ./RELEASE_NOTES.md + } else { + goreleaser build --snapshot --clean --single-target + } + Get-Command "./dist/tstoy*/tstoy*" -ErrorAction Stop +} + +switch ($Target) { + 'build' { + $Application = Build-Project + if ($AddToPath) { + $ApplicationFolder = Split-Path -Parent $Application.Path + $PathSeparator = [System.IO.Path]::PathSeparator + if ($ApplicationFolder -notin ($env:PATH -split $PathSeparator)) { + $env:PATH = $ApplicationFolder + $PathSeparator + $env:PATH + } + } + if ($Initialize) { + $Alias = Set-Alias -Name tstoy -Value $Application.Path -PassThru + Invoke-Expression $(tstoy completion powershell | Out-String) + $Alias + } else { + $Application + } + } + 'package' { + Build-Project -All + } + 'test' { + $Application = Build-Project + $TestContainer = New-PesterContainer -Path 'acceptance.tests.ps1' -Data @{ + Application = $Application + } + Invoke-Pester -Container $TestContainer -Output Detailed + } +} \ No newline at end of file diff --git a/app/cmd/path.go b/app/cmd/path.go new file mode 100644 index 0000000..b822d6c --- /dev/null +++ b/app/cmd/path.go @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + "path/filepath" + + "github.com/PowerShell/DSC-Samples/app/config" + "github.com/spf13/cobra" +) + +// PathCmd represents the path command +var PathCmd = &cobra.Command{ + Use: "path", + Short: "Retrieves the path to the machine and user configs", + Long: `You can use this command to retrieve the path to the configuration + files that the application looks for on your system. + + If you don't specify any arguments for this command, it returns the paths + to both files, with the machine scope configuration file first.`, + Run: showPath, + ValidArgs: []string{"machine", "user"}, + Args: cobra.MatchAll(cobra.MaximumNArgs(2), cobra.OnlyValidArgs), +} + +func init() { + ShowCmd.AddCommand(PathCmd) +} + +func showPath(cmd *cobra.Command, args []string) { + if len(args) == 0 { + printConfigPath("machine") + printConfigPath("user") + } else { + for _, scope := range args { + printConfigPath(scope) + } + } +} + +func printConfigPath(scope string) { + folder := config.MachineFolder + if scope == "user" { + folder = config.UserFolder + } + path := filepath.Join(folder, config.FileName) + path = fmt.Sprintf("%s.%s", path, config.FileExtension) + fmt.Println(path) +} diff --git a/app/cmd/root.go b/app/cmd/root.go new file mode 100644 index 0000000..93443cb --- /dev/null +++ b/app/cmd/root.go @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + "os" + + "github.com/PowerShell/DSC-Samples/app/config" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Build-time variables +var ( + // The version of the app - usually the tagged version + version = "dev" + // The most recent commit for the build + commit = "none" + // The date the app was built + date = "unknown" +) + +var cfgFile string +var autoUpdate bool +var checkFrequency int +var MachineConfig map[string]any +var UserConfig map[string]any + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "tstoy", + Short: "A fictional application to demonstrate writing a DSC Resource.", + Long: `This is a fictional application with a handful of configuration +options defined so you can write a Desired State Configuration (DSC) Resource +against a known application. + +While a real application could define its own manifest for a DSC Resource, +tstoy does not. Instead, the DSC documentation includes a number of example +DSC Resources all managing this application but written in different +programming languages.`, + Version: version, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := RootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Users can pass an explicit config. + RootCmd.PersistentFlags().StringVar( + &cfgFile, + "config", + "", + "config file (default is the machine configuration, then user configuration)", + ) + + RootCmd.PersistentFlags().BoolVar( + &autoUpdate, + "update-automatically", + false, + "Specifies whether the app should update automatically if the frequency window is past.", + ) + viper.BindPFlag("updates.automatic", RootCmd.PersistentFlags().Lookup("update-automatically")) + + RootCmd.PersistentFlags().IntVar( + &checkFrequency, + "update-frequency", + config.Default.Updates.CheckFrequency, + "Specifies the length of the frequency window for updates in days.", + ) + viper.BindPFlag("updates.checkFrequency", RootCmd.PersistentFlags().Lookup("update-frequency")) +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + viper.SetDefault("updates.automatic", config.Default.Updates.Automatic) + viper.SetDefault("updates.checkFrequency", config.Default.Updates.CheckFrequency) + + viper.AutomaticEnv() // read in environment variables that match + + // Check for and merge-in the config settings for the machine, then user configs. + machineConfig := getScopedConfigFile(config.Machine) + if nil != machineConfig { + viper.MergeConfigMap(machineConfig) + } + userConfig := getScopedConfigFile(config.User) + if nil != userConfig { + viper.MergeConfigMap(userConfig) + } + + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } + } +} + +func getScopedConfigFile(scope config.Scope) map[string]any { + folder := config.MachineFolder + if scope == config.User { + folder = config.UserFolder + } + + v := viper.New() + v.AddConfigPath(folder) + v.SetConfigName(config.FileName) + v.SetConfigType(config.FileExtension) + + // If a config file exists, read it in. + if err := v.ReadInConfig(); err == nil { + vConfig := config.FromMap(v.AllSettings()) + if scope == config.User { + UserConfig = vConfig.ToMap() + } else { + MachineConfig = vConfig.ToMap() + } + return v.AllSettings() + } + + return nil +} diff --git a/app/cmd/show.go b/app/cmd/show.go new file mode 100644 index 0000000..3c03d98 --- /dev/null +++ b/app/cmd/show.go @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + + "github.com/PowerShell/DSC-Samples/app/config" + "github.com/TylerBrock/colorjson" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/exp/slices" +) + +var only []string + +// ShowCmd represents the show command +var ShowCmd = &cobra.Command{ + Use: "show", + Short: "Shows the merged configuration options for the application.", + Long: `This command shows the merged configuration options for the + application. The settings are applied first from the machine-level, + then the user-level, then any environment variables, and finally any + flags passed to the command.`, + Run: run, + Args: cobra.MatchAll(cobra.NoArgs), +} + +func init() { + RootCmd.AddCommand(ShowCmd) + + ShowCmd.Flags().StringSliceVar( + &only, + "only", + []string{"default", "machine", "user", "final"}, + "The list of configuration values to retrieve. Valid values are 'default', 'machine', 'user', and 'final'.", + ) + ShowCmd.RegisterFlagCompletionFunc("only", configTargetCompletion) +} + +func run(cmd *cobra.Command, args []string) { + if len(only) == 1 { + printConfigOnly(only[0]) + return + } + if slices.Contains(only, "default") { + printConfig(config.Default.ToMap(), "Default", defaultConfig) + } + if slices.Contains(only, "machine") { + printConfig(MachineConfig, "Machine", configFile) + } + if slices.Contains(only, "user") { + printConfig(UserConfig, "User", configFile) + } + if slices.Contains(only, "final") { + printConfig(getFinalConfig(), "Final", final) + } +} + +func configTargetCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{ + "default\tReturns the default configuration settings.", + "machine\tReturns the machine-scope configuration settings.", + "user\tReturns the user-scope configuration settings.", + "final\tReturns the final configuration after all mergings.", + }, cobra.ShellCompDirectiveDefault +} + +var baseStyle = lipgloss.NewStyle().Bold(true) +var defaultStyle = baseStyle.Copy().Foreground(lipgloss.Color("#698F3F")) +var configFileStyle = baseStyle.Copy().Foreground(lipgloss.Color("#1F5673")) +var finalStyle = baseStyle.Copy().Foreground(lipgloss.Color("#DE4D86")) + +type configGroup int + +const ( + defaultConfig configGroup = iota + configFile + final +) + +func printConfigOnly(target string) { + var targetConfig any + switch target { + case "machine": + targetConfig = MachineConfig + case "user": + targetConfig = UserConfig + case "final": + targetConfig = getFinalConfig() + default: + targetConfig = config.Default.ToMap() + } + + getJsonFormatter() + formatted, _ := formatter.Marshal(targetConfig) + fmt.Println(string(formatted)) +} + +func printConfig(value any, name string, group configGroup) { + getJsonFormatter() + formatted, _ := formatter.Marshal(value) + var style lipgloss.Style + switch group { + case defaultConfig: + style = defaultStyle + case final: + style = finalStyle + case configFile: + style = configFileStyle + default: + style = defaultStyle + } + prefix := fmt.Sprintf("%s configuration:", name) + prefix = style.Render(prefix) + fmt.Println(prefix, string(formatted)) +} + +var formatter *colorjson.Formatter + +func getJsonFormatter() { + if nil == formatter { + formatter = colorjson.NewFormatter() + formatter.Indent = 2 + } +} + +func getFinalConfig() map[string]any { + vConfig := config.FromMap(viper.AllSettings()) + + return vConfig.ToMap() +} diff --git a/app/config/config.go b/app/config/config.go new file mode 100644 index 0000000..3dd5a1f --- /dev/null +++ b/app/config/config.go @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package config + +import ( + "encoding/json" + "path/filepath" + + "github.com/adrg/xdg" +) + +type Options struct { + Updates Updates `mapstructure:"updates,omitempty"` +} + +type Updates struct { + Automatic bool `mapstructure:"automatic,omitempty"` + CheckFrequency int `mapstructure:"checkFrequency,omitempty"` +} + +var Default = Options{ + Updates: Updates{ + Automatic: false, + CheckFrequency: 90, + }, +} + +func (o *Options) ToJson() ([]byte, error) { + return json.Marshal(o) +} + +func (o *Options) ToMap() map[string]any { + var m map[string]any + + jsonBytes, _ := o.ToJson() + json.Unmarshal(jsonBytes, &m) + + // need to remove the CheckFrequency value if it's zero, because that's + // functionally unset. + updates, ok := m["Updates"].(map[string]any) + if ok { + if updates["CheckFrequency"].(float64) == 0 { + delete(updates, "CheckFrequency") + m["Updates"] = updates + } + } + + return m +} + +// Converts the viper map of settings to an instance of Options. This is +// required for handling how viper fully downcases keys. It ensures that +// the show command can display a consistent set of values from the +// configuration options. +func FromMap(data map[string]any) Options { + options := Options{} + updates, ok := data["updates"].(map[string]any) + if !ok { + return options + } + + auto, ok := updates["automatic"].(bool) + if ok { + options.Updates.Automatic = auto + } + + // if the map came from JSON, it's a float64 + floatFreq, ok := updates["checkfrequency"].(float64) + if ok && floatFreq != 0 { + options.Updates.CheckFrequency = int(floatFreq) + } + + // if the map came from viper, it's an integer + intFreq, ok := updates["checkfrequency"].(int) + if ok && intFreq != 0 { + options.Updates.CheckFrequency = intFreq + } + + return options +} + +type Scope int + +const ( + Machine Scope = iota + User +) + +var MachineFolder string = getConfigFolder(Machine) +var UserFolder string = getConfigFolder(User) +var AppFolder string = filepath.Join("TailSpinToys", "tstoy") + +const FileName string = "tstoy.config" +const FileExtension string = "json" + +func getConfigFolder(scope Scope) string { + if scope == Machine { + return filepath.Join(xdg.ConfigDirs[0], AppFolder) + } + + return filepath.Join(xdg.ConfigHome, AppFolder) +} diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..345cdf7 --- /dev/null +++ b/app/go.mod @@ -0,0 +1,40 @@ +module github.com/PowerShell/DSC-Samples/app + +go 1.19 + +require ( + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 + github.com/adrg/xdg v0.4.0 + github.com/charmbracelet/lipgloss v0.7.1 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.16.0 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..0b00f28 --- /dev/null +++ b/app/go.sum @@ -0,0 +1,522 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..80824a2 --- /dev/null +++ b/app/main.go @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package main + +import "github.com/PowerShell/DSC-Samples/app/cmd" + +func main() { + cmd.Execute() +} diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 0000000..00f4623 --- /dev/null +++ b/docs/_index.md @@ -0,0 +1,24 @@ +--- +title: Home +platen: + title_as_heading: false +--- + +## DSC samples and tutorials + +Starting with v3, Microsoft's Desired State Configuration (DSC) supports implementing DSC Resources +in any language as command-based resources. + +This site includes sample implementations for DSC Resources and accompanying documentation. For a +list of tutorials by purpose, with sample implementation in multiple languages, see +[Tutorials][01]. For a list of tutorials by language, see [Languages][02]. + +You can find more information about DSC in the [official documentation][03]. + +If you're interested in filing bugs, requesting samples or tutorials, or otherwise contributing, +see [Contributing to the DSC Samples][04]. + +[01]: tutorials/_index.md +[02]: languages/_index.md +[03]: https://learn.microsoft.com/powershell/dsc/overview?view=dsc-3.0 +[04]: contributing/_index.md diff --git a/docs/contributing/_index.md b/docs/contributing/_index.md new file mode 100644 index 0000000..bb20417 --- /dev/null +++ b/docs/contributing/_index.md @@ -0,0 +1,30 @@ +--- +title: Contributing to the DSC Samples +linktitle: Contributing +weight: 100 +platen: + menu: + collapse_section: true +--- + +The DSC samples repository includes the source code for tutorials and reference implementations. +Your contributions are welcome in whatever form they take - requests for new tutorials, fixes for +implementations, new language contributions for an existing tutorial, or something else. + +This section contains guidance to help you effectively contribute. + +## Ways to contribute + +There are a few ways to contribute. All contributions are valuable to us. + +- Filing issues helps us identify problems and gaps in our documentation. Sometimes the issues are + difficult to resolve, requiring more investigation and research. The issue process allows us to + have a conversation abou the problem and develop a satisfactory resolution. +- Submitting a pull request to add or change content and code is a more involved process. The + following information outlines the tools, processes, and standards for submitting content and + code to the samples repository. + +If you're interested in submitting a new tutorial or adding an implementation to an existing one, +see [Contributing a tutorial][01]. + +[01]: ./tutorials/_index.md diff --git a/docs/contributing/filing-issues.md b/docs/contributing/filing-issues.md new file mode 100644 index 0000000..371ada4 --- /dev/null +++ b/docs/contributing/filing-issues.md @@ -0,0 +1,71 @@ +--- +title: How to file an issue +linktitle: Filing Issues +weight: 10 +--- + +This project supports filing three kinds of issues: bug reports, new tutorial requests, and new +tutorial implementation requests. + +Before you file any issue, make sure to follow these steps: + +1. Search the existing issues for this repository. If there is an issue that fits your needs, don't + file a new one. Subscribe, react, or comment on that issue instead. +1. Think of a descriptive title. Issue titles should be a short synopsis of the problem or request + to help people orient themselves when looking at the issue list. +1. If you're requesting a new tutorial or a new implementation of an existing tutorial, read the + information about [contributing a tutorial][01] + +## Bug reports { .text-center toc_md="Report a bug" } + +``````columns { #bug-report-info } +```column { grow=2 } +To report issues with a tutorial or code sample, like typos, technical and +factual errors, and grammar, use the following button: +``` + +```column { .flex .align-center .justify-center .text-center } +![button:File a bug report][br] +{ variant="danger" prefix_icon="bug-fill" pill=true } + +[br]: https://github.com/PowerShell/DSC-Samples/issues/new?assignees=&labels=bug&projects=&template=00-bug.yml +``` +`````` + +## Requesting a new tutorial { .text-center toc_md="New tutorial" } + +``````columns { #new-tutorial-info } +```column { grow=2 } +To request a new tutorial, consider the learning goals for the tutorial. Why is +it needed? What gap will it fgill? Who is the intended audience? What scenario +will it address? + +When you have a concrete idea for the new tutorial, use the following button +to submit your request: +``` + +```column { .flex .align-center .justify-center .text-center } +![button:Request a new tutorial][br] +{ variant="success" prefix_icon="file-plus-fill" pill=true } + +[br]: https://github.com/PowerShell/DSC-Samples/issues/new?assignees=&labels=request-tutorial%20needs-triage&projects=&template=01-new-tutorial.yml +``` +`````` + +## Requesting a new tutorial implementation { .text-center toc_md="New implementation" } + +``````columns { #new-implementation-info } +```column { grow=2 } +To request a new language implementation for an existing tutorial, use the +following button to submit your request: +``` + +```column { .flex .align-center .justify-center .text-center } +![button:Request a new implementation][br] +{ variant="primary" prefix_icon="file-earmark-code-fill" pill=true } + +[br]: https://github.com/PowerShell/DSC-Samples/issues/new?assignees=&labels=request-tutorial%20needs-triage&projects=&template=01-new-tutorial.yml +``` +`````` + +[01]: tutorials/_index.md diff --git a/docs/contributing/submitting-prs.md b/docs/contributing/submitting-prs.md new file mode 100644 index 0000000..74a8eb5 --- /dev/null +++ b/docs/contributing/submitting-prs.md @@ -0,0 +1,66 @@ +--- +title: How to submit a PR +linktitle: Submitting PRs +weight: 20 +--- + +To make changes to content, submit a pull request (PR) from your fork. A pull request must be +reviewed before it can be merged. + +## Using git branches + +The default branch for the DSC samples is the `main` branch. Changes made in working branches are +merged into the `main`` branch, which automatically publishes the website. + +Before starting any changes, create a working branch in your local copy of the DSC samples +repository. When working locally, be sure to synchronize your local repository before creating your +working branch. The working branch should be created from an update-to-date copy of the main +branch. + +All pull requests should target the `main` branch. + +## Improve the pull request process for everyone { toc_md="Best Practices" } + +The simpler and more focused you can make your PR, the faster it can be reviewed and merged. + +### Avoid pull requests that contain unrelated changes { toc_md="Avoid unrelated changes" } + +Avoid creating PRs that contain unrelated changes. Separate minor updates to existing articles from +new articles or major rewrites. Work on these changes in separate working branches. + +### Avoid editing repository configuration files { toc_md="Don't change configuration" } + +Avoid modifying repository. Limit your changes where possible to the Markdown content files, +supporting image files, and the `sample` folder. + +Incorrect modifications to repository configuration files can break the build, introduce +vulnerabilities or accessibility issues, or violate organizational standards. Repository +configuration files are any files that match one or more of these patterns: + +- `_site/**` +- `.github/**` +- `.gitattributes` +- `LICENSE` +- `.markdownlint*` + + diff --git a/docs/contributing/tutorials/_index.md b/docs/contributing/tutorials/_index.md new file mode 100644 index 0000000..aac0e24 --- /dev/null +++ b/docs/contributing/tutorials/_index.md @@ -0,0 +1,38 @@ +--- +title: Contributing a tutorial +linktitle: Tutorials +weight: 30 +--- + +The samples repository includes two types of tutorials. + +The first type of tutorial is the general DSC tutorial. These tutorials document how to use DSC +itself for different tasks and contexts. + +The other type of tutorial is the DSC Resource tutorial. These tutorials show how a user can author +a resource or extend an existing resource's functionality. The resource tutorials are organized by +learning goal. Each tutorial may have any number of language-specific implementations. Every +implementation for a given tutorial must result in the same functionality and learned lessons when +a reader completes the tutorial. + +When a new resource tutorial is created, a tutorial specification is documented for it. If you're +contributing a new tutorial, the documentation team will work with you on the specification. You +don't need to write the specification yourself. + +## Submitting a tutorial + +To submit a new tutorial of any type, first file an issue in the repository. The documentation team +will work with you on your submission to help refine it. You can just file requests for new +tutorials without any intent to author the tutorial yourself. + +## Adding a tutorial implementation + +To submit a new language implementation for an existing resource tutorial, follow these steps: + +1. Read the specification documentation for that tutorial. Your sample code must fulfill the + tutorial's specification. +1. Implement the sample code to the tutorial's specifications. +1. Submit the sample code as a pull request and indicate whether you're authoring the draft of the + tutorial yourself. The author of the sample code for a tutorial implementation doesn't need to + author the documentation for it. +1. Collaborate with the documentation team to refine your sample code and the documentation. diff --git a/docs/contributing/tutorials/resource-specs/_index.md b/docs/contributing/tutorials/resource-specs/_index.md new file mode 100644 index 0000000..f6eccc5 --- /dev/null +++ b/docs/contributing/tutorials/resource-specs/_index.md @@ -0,0 +1,8 @@ +--- +title: DSC Resource tutorial implementation specifications +linktitle: Specifications +weight: 100 +platen: + menu: + collapse_section: true +--- diff --git a/docs/contributing/tutorials/resource-specs/first-resource.md b/docs/contributing/tutorials/resource-specs/first-resource.md new file mode 100644 index 0000000..d1d1346 --- /dev/null +++ b/docs/contributing/tutorials/resource-specs/first-resource.md @@ -0,0 +1,194 @@ +--- +title: Write your first DSC Resource Specification +linktitle: Write a DSC Resource +weight: 10 +# platen: +# menu: +# collapse_section: true +--- + +## Synopsis + +Specification for the sample code used in the [Write your first DSC Resource][01] tutorials. + +## Description + +This tutorial series uses the following learning goals: + +- Create a small `` application to use as a DSC Resource. +- Define the properties of the DSC Resource. +- Implement get and set commands for the DSC Resource. +- Write a manifest for the DSC Resource. +- Manually test the DSC Resource. + +The tutorial walks a reader through creating a basic DSC Resource to manage the fictional +[TSToy][02] application to meet those learning goals. + +## Sample code requirements + +The sample code representing the end of the tutorial must: + +1. Define properties to manage the machine and user scope configurations for the TSToy application. +1. When called outside of DSC, handle input from stdin and argument flags. +1. Implement the `get` method. +1. Implement the `set` method. +1. Not implement the `test` method. +1. Include a DSC Resource manifest. + +### Property definitions + +The completed sample code must define the following properties: + +- `scope` as a required property for the target configuration scope. This property must be an enum + that accepts the values `machine` and `user`. +- `ensure` as an optional property that defaults to `present`. This property should define whether + the DSC Resource creates or deletes the configuration file for the target scope. This property + must be an enum that accepts the values `present` and `absent`. +- `updateAutomatically` as an optional property that manages the configuration file's + `updates.automatic` setting. This property must be a boolean type. +- `updateFrequency` as an optional property that manages the configuration file's + `updates.frequency` setting. This property must be an integer that accepts a minimum value of 1 + and a maximum value of `90`. + +The JSON serialization of an instance should always include the `scope` and `ensure` properties. It +should only include the `updateAutomatically` or `updateFrequency` property if those values are set +in the configuration. For example: + +```json +// when the config is absent +{"ensure":"absent","scope":"machine"} +// When update.automatic is defined but update.frequency isn't +{"ensure":"present","scope":"machine","updateAutomatically":false} +// When all options are defined +{"ensure":"present","scope":"user","updateAutomatically":true,"updateFrequency":45} +``` + +### Input handling + +The application must accept an instance of the DSC Resource as a JSON blob over stdin and as the +value of the `--inputJSON` parameter for the `get` and `set` commands. + +The application must accept the following parameters for the `get` and `set` commands to define the +resource instance's properties: + +- `--scope` +- `--ensure` +- `--updateAutomatically` +- `--updateFrequency` +- `--inputJSON` + +The `get` method must also support the `--all` boolean flag for returning every instance of the +resource. + +### Get method implementation + +The application's `get` method must: + +1. Raise an error when the input JSON doesn't include the `scope` property and neither the + `--scope` nor `--all` parameters are specified. +1. Return an instance for the machine and user scope configurations when the `--all` parameter flag + is specified. +1. If the input JSON includes the `scope` parameter and the `--scope` argument is specified, use + the value of the `--scope` parameter. +1. Ignore any other arguments and properties in the input JSON. +1. Return an instance for the specified scope as a JSON blob on a single line without any + whitespace. +1. Use the [tstoy show path][03] command to retrieve the path to the configuration file for the + specified scope and error if the `tstoy` application isn't executable. + +### Set method implementation + +The application's `set` method must: + +1. Raise an error when the input JSON doesn't include the `scope` property and the `--scope` + parameter isn't specified. +1. Raise an error if any of the specified properties for the desired state are invalid. +1. Support three set operations: + - Create the configuration file if it doesn't exist and should. When the file is created, it + must include only the defined settings for the desired state. + - Remove the configuration file if it does exist and shouldn't. + - Update the configuration file if it does and should exist but the `update.automatic` or + `update.frequency` settings are out of the desired state. When the resource updates the + configuration file, it must not change the order or value of any unmanaged settings. +1. If the instance is in the desired state, the resource must not alter the configuration file in + any way. +1. Return the actual state of the configuration file as an instance after the set operation as a + JSON blob on a single line without any whitespace. +1. Use the [tstoy show path][03] command to retrieve the path to the configuration file for the + specified scope and error if the `tstoy` application isn't executable. + +### Manifest definition + +The DSC Resource manifest use the following template, replacing the placeholder values as needed: + +- `` - the two-character language prefix for the tutorial implementation, like + `py` for Python or `rs` for Rust. +- `` - the language for the tutorial implementation, like `Python` or `Rust`. + +```json +{ + "manifestVersion": "1.0", + "type": "TSToy.Example/tstoy", + "version": "0.1.0", + "description": "A DSC Resource written in to manage TSToy.", + "get": { + "executable": "tstoy", + "args": ["get"], + "input": "stdin" + }, + "set": { + "executable": "tstoy", + "args": ["set"], + "input": "stdin", + "preTest": false, + "return": "state" + }, + "schema": { + "embedded": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": " TSToy Resource", + "type": "object", + "required": [ + "scope" + ], + "properties": { + "scope": { + "title": "Target configuration scope", + "description": "Defines which of TSToy's config files to manage.", + "type": "string", + "enum": [ + "machine", + "user" + ] + }, + "ensure": { + "title": "Ensure configuration file existence", + "description": "Defines whether the config file should exist.", + "type": "string", + "enum": [ + "present", + "absent" + ], + "default": "present" + }, + "updateAutomatically": { + "title": "Should update automatically", + "description": "Indicates whether TSToy should check for updates when it starts.", + "type": "boolean" + }, + "updateFrequency": { + "title": "Update check frequency", + "description": "Indicates how many days TSToy should wait before checking for updates.", + "type": "integer", + "minimum": 1, + "maximum": 90 + } + } + } + } +} +``` + +[01]: /DSC-Samples/tutorials/first-resource/ +[02]: /DSC-Samples/tstoy/about/ +[03]: /DSC-Samples/tstoy/cli/show-path/ diff --git a/docs/languages/_index.md b/docs/languages/_index.md new file mode 100644 index 0000000..5cf1122 --- /dev/null +++ b/docs/languages/_index.md @@ -0,0 +1,14 @@ +--- +title: Languages +weight: 30 +platen: + menu: + collapse_section: true +--- + +This section includes a set of languages folders. Each folder includes one or more tutorials +implemented in that language. + +For a list of tutorials using the DSC samples by purpose instead, see [tutorials][01]. + +[01]: ../tutorials/_index.md diff --git a/docs/languages/go/_index.md b/docs/languages/go/_index.md new file mode 100644 index 0000000..e39cd9d --- /dev/null +++ b/docs/languages/go/_index.md @@ -0,0 +1,8 @@ +--- +title: DSC and Go tutorials +dscs: + languages_title: Golang +platen: + menu: + collapse_section: true +--- diff --git a/docs/tstoy/_index.md b/docs/tstoy/_index.md new file mode 100644 index 0000000..c8b9540 --- /dev/null +++ b/docs/tstoy/_index.md @@ -0,0 +1,8 @@ +--- +title: About the TSToy application +linktitle: TSToy +weight: 10 +platen: + menu: + collapse_section: true +--- diff --git a/docs/tstoy/about.md b/docs/tstoy/about.md new file mode 100644 index 0000000..d6e5d99 --- /dev/null +++ b/docs/tstoy/about.md @@ -0,0 +1,193 @@ +--- +title: About the TSToy application +linktitle: About +--- + +## Overview + +For the purposes of the command-based DSC Resource tutorials, you're creating a DSC Resource to +manage the fictional Tailspin Toys application, `tstoys`. + +The application has configuration options that control whether it should look for updates and how +frequently to do so. Like many real applications, `tstoy` uses a combination of arguments, +environment variables, and configuration files. For these tutorials, the DSC Resource only needs to +manage the configuration files. + +## Installing TSToy + +[Download the latest release][01] for your operating system. After you download the release +archive, you need to expand the archive and add it to your PATH. You'll need the application while +following any of the tutorials in this section. + + + +Once you have `tstoy` installed and added to your path, you can call it to see the available +commands: + +```sh +tstoy +``` + +You can enable shell completions for the application to make interacting with it easier. + +```bash +# bash +tstoy completion bash --help +source <(tstoy completion bash) +``` + +```sh +# fish +tstoy completion fish --help +tstoy completion fish | source +``` + +```powershell +# PowerShell +tstoy completion powershell --help +tstoy completion powershell | Out-String | Invoke-Expression +``` + +```zsh +# zsh +tstoy completion zsh --help +source <(tstoy completion zsh) +``` + +## TSToy configuration + +The TSToy application uses two configuration files. The configuration for all users is the +_machine_-scope configuration file. The configuration for the current user is the _user_-scope +configuration file. + +When TSToy runs, it starts with a default configuration. If the machine-scope configuration file +exists, TSToy overrides the default configuration with the settings in that file. Then, if the +user-scope configuration file exists, TSToy overrides the configuration with the settings in that +file. + +The DSC Resource needs to be able to manage both the machine and user scope configuration files. + +TSToy expects the configuration files to be JSON files. It uses settings in the `updates` +key of that JSON file to control the update behavior. + +TSToy uses this default configuration: + +```json +{ + "updates": { + "automatic": true, + "checkFrequency": 90 + } +} +``` + +When `automatic` is set to `true`, TSToy looks for updates when it starts. The value of +`checkFrequency` indicates how many days it should wait before looking for updates +again. TSToy only considers the `checkFrequency` setting valid when it's an integer +between `1` and `90`, inclusive. + +Your DSC Resource needs to manage: + +- Whether the configuration file in a specific scope should exist +- Whether it sets the configuration to automatically update +- How many days it sets the configuration to wait before looking for updates + +## TSToy commands + +While working through the tutorials, you'll need to retrieve the configuration files path that +`tstoy` uses. You can get the paths to the configuration files with the `show path` command: + +```sh +# Outputs both paths, with the machine-scope configuration file first. +tstoy show path +# Outputs only the path to the machine-scope configuration file. +tstoy show path machine +# Outputs only the path to the user-scope configuration file. +tstoy show path user +``` + +``````tabs { #command-output} +````tab { name="On Windows" } +```powershell +tstoy show path +``` +```text +C:\ProgramData\TailSpinToys\tstoy\tstoy.config.json +C:\Users\mikey\AppData\Local\TailSpinToys\tstoy\tstoy.config.json +``` +```powershell +tstoy show path machine +``` +```text +C:\ProgramData\TailSpinToys\tstoy\tstoy.config.json +``` +```powershell +tstoy show path user +``` +```text +C:\Users\mikey\AppData\Local\TailSpinToys\tstoy\tstoy.config.json +``` +```` + +````tab { name="On Ubuntu" } +```sh +tstoy show path +``` +```text +/etc/xdg/TailSpinToys/tstoy/tstoy.config.json +/home/mikey/.config/TailSpinToys/tstoy/tstoy.config.json +``` +```sh +tstoy show path machine +``` +```text +/etc/xdg/TailSpinToys/tstoy/tstoy.config.json +``` +```sh +tstoy show path user +``` +```text +/home/mikey/.config/TailSpinToys/tstoy/tstoy.config.json +``` +```` +`````` + +You may also want to see how TSToy is interpreting the configuration files to validate your work. +Use the `show` command to get the application's default settings, the settings from each +configuration scope, and the final merged settings. + +```sh +# Show all settings +tstoy show +``` + +```yaml +Default configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } +} +Machine configuration: {} +User configuration: {} +Final configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } +} +``` + +Use the `--only` flag to get a subset of configuration. + +```sh +tstoy show --only machine,user +``` + +```yaml +Machine configuration: {} +User configuration: {} +``` + + +[01]: https://github.com/MicrosoftDocs/DSC-Examples/releases/tag/app%2Fv1.0.0 diff --git a/docs/tstoy/cli/_index.md b/docs/tstoy/cli/_index.md new file mode 100644 index 0000000..8d53087 --- /dev/null +++ b/docs/tstoy/cli/_index.md @@ -0,0 +1,6 @@ +--- +title: Command Reference +platen: + menu: + collapse_section: true +--- diff --git a/docs/tstoy/cli/root.md b/docs/tstoy/cli/root.md new file mode 100644 index 0000000..828b604 --- /dev/null +++ b/docs/tstoy/cli/root.md @@ -0,0 +1,58 @@ +--- +title: "`tstoy`" +weight: 1 +--- + +## Synopsis + +A fictional application to demonstrate writing a DSC Resource. + +## Description + +This is a fictional application with a handful of configuration options defined so you can write a +Desired State Configuration (DSC) Resource against a known application. + +While a real application could define its own manifest for a DSC Resource, tstoy doesn't. Instead, +the DSC documentation includes example DSC Resources all managing this application but written in +different programming languages. + +## Options + +```cli-syntax + --config string config file +-h, --help help for tstoy + --update-automatically Specifies whether the app should update automatically + --update-frequency int Specifies the frequency window for updates in days. +``` + +### `--config` { #option-config } + +This parameter enables you to specify a configuration file to use for the application. When +this parameter isn't specified, the application merges the user-scope configuration over the +machine-scope configuration. + +### `-h` / `--help` { #option-help toc_md="`--help`" } + +This parameter displays the help info for the current command. When you specify this parameter, the +application ignores all other parameters. + +### `--update-automatically` { #option-update-automatically } + +This flag parameter specifies whether the application should update automatically if the update +frequency window has elapsed. When you specify this flag, the value overrides any settings from +configuration. If you specify this flag without the `=` suffix, the value is `true`. + +To set automatic updates to `false` with this parameter, use this syntax: + +```sh +tstoy --update-automatically=false +``` + +### `--update-frequency` { #option-update-frequency } + +This parameter specifies the length of the application's update frequency window in days. When you +specify this parameter, the value overrides any settings from configuration. + +## Related links + +- [tstoy show](./show/_index.md) - Shows the merged configuration options for the application. diff --git a/docs/tstoy/cli/show-path.md b/docs/tstoy/cli/show-path.md new file mode 100644 index 0000000..f3dec92 --- /dev/null +++ b/docs/tstoy/cli/show-path.md @@ -0,0 +1,74 @@ +--- +title: "`tstoy show path`" +link title: "`path`" +weight: 3 +--- + +## Synopsis + +Retrieves the path to the machine and user configs + +## Description + +You can use this command to retrieve the path to the configuration files that the application looks +for on your system. + +If you don't specify any arguments for this command, it returns the paths to both files, with the +machine scope configuration file first. + +```cli-syntax +tstoy show path [option flags] [scope] +``` + +## Scope + +This command accepts a single argument specifying which configuration file path to return. When you +specify `machine`, the command returns the path to the machine-scope configuration file on your +computer. When you specify `user`, the command returns the path to the user-scope configuration +file. + +## Options + +```cli-syntax + -h, --help help for path +``` + +### `-h` / `--help` { #option-help toc_md="`--help`" } + +This parameter displays the help info for the current command. When you specify this parameter, the +application ignores all other parameters. + +## Inherited options + +```cli-syntax + --config string config file + --update-automatically Specifies whether the app should update automatically + --update-frequency int Specifies the frequency window for updates in days. +``` + +### `--config` { #inherited-option-config } + +This parameter enables you to specify a configuration file to use for the application. When +this parameter isn't specified, the application merges the user-scope configuration over the +machine-scope configuration. + +### `--update-automatically` { #inherited-option-update-automatically } + +This flag parameter specifies whether the application should update automatically if the update +frequency window has elapsed. When you specify this flag, the value overrides any settings from +configuration. If you specify this flag without the `=` suffix, the value is `true`. + +To set automatic updates to `false` with this parameter, use this syntax: + +```sh +tstoy --update-automatically=false +``` + +### `--update-frequency` { #inherited-option-update-frequency } + +This parameter specifies the length of the application's update frequency window in days. When you +specify this parameter, the value overrides any settings from configuration. + +## Related links + +- [tstoy show](./_index.md) - Shows the merged configuration options for the application. diff --git a/docs/tstoy/cli/show.md b/docs/tstoy/cli/show.md new file mode 100644 index 0000000..7ef347c --- /dev/null +++ b/docs/tstoy/cli/show.md @@ -0,0 +1,79 @@ +--- +title: "`tstoy show`" +link title: "`show`" +weight: 2 +--- + +## Synopsis + +Shows the merged configuration options for the application. + +## Description + +This command shows the merged configuration options for the application. The settings are applied +first from the machine-level, then the user-level, then any environment variables, and finally any +flags passed to the command. + +``` +tstoy show [flags] +``` + +## Options + +```cli-syntax +-h, --help help for show + --only strings The list of configuration values to retrieve. +``` + +### `-h` / `--help` { #option-help toc_md="`--help`" } + +This parameter displays the help info for the current command. When this parameter is specified, +the application ignores all other parameters. + +### `--only` { #option-only } + +This parameter filters the list of configuration values to display. Valid values are: + +- `default` - The application's default configuration, independent of any configuration files. +- `machine` - The machine-scope configuration file settings. +- `user` - The user-scope configuration file settings. +- `final` - The effective configuration, after merging the defined configuration files over the + default configuration. + +By default, the command shows every configuration value. + +## Inherited options + +```cli-syntax + --config string config file + --update-automatically Specifies whether the app should update automatically + --update-frequency int Specifies the frequency window for updates in days. +``` + +### `--config` { #inherited-option-config } + +This parameter enables you to specify a configuration file to use for the application. When +this parameter isn't specified, the application merges the user-scope configuration over the +machine-scope configuration. + +### `--update-automatically` { #inherited-option-update-automatically } + +This flag parameter specifies whether the application should update automatically if the update +frequency window has elapsed. When you specify this flag, the value overrides any settings from +configuration. If you specify this flag without the `=` suffix, the value is `true`. + +To set automatic updates to `false` with this parameter, use this syntax: + +```sh +tstoy --update-automatically=false +``` + +### `--update-frequency` { #inherited-option-update-frequency } + +This parameter specifies the length of the application's update frequency window in days. When you +specify this parameter, the value overrides any settings from configuration. + +## Related links + +- [tstoy](../_index.md) - A fictional application to demonstrate writing a DSC Resource. +- [tstoy show path](show-path.md) - Retrieves the path to the machine and user configs. diff --git a/docs/tutorials/_index.md b/docs/tutorials/_index.md new file mode 100644 index 0000000..ad1a70e --- /dev/null +++ b/docs/tutorials/_index.md @@ -0,0 +1,14 @@ +--- +title: Tutorials +weight: 20 +platen: + menu: + collapse_section: true +--- + +This section includes a set of tutorials, organized by purpose. Each tutorial may be implemented in +any number of different languages. + +For a list of tutorials using the DSC samples by language instead, see [languages][01]. + +[01]: ../languages/_index.md diff --git a/docs/tutorials/first-resource/_index.md b/docs/tutorials/first-resource/_index.md new file mode 100644 index 0000000..2219fe4 --- /dev/null +++ b/docs/tutorials/first-resource/_index.md @@ -0,0 +1,24 @@ +--- +title: Write your first DSC Resource +dscs: + tutorials_title: Write a DSC Resource +platen: + menu: + collapse_section: true +--- + +Each of the tutorials in this section implements the same DSC Resource in a different language. + +The DSC Resources manage the fictional [TSToy application][01]'s configuration files. While the +TSToy application isn't a real production application, managing configuration files is a common +use-case for DSC Resources. + +In these tutorials, you learn how to: + +- Create a DSC Resource in your preferred programming language. +- Define the properties of the resource. +- Implement get and set commands for the resource. +- Write a manifest for the resource. +- Manually test the resource. + +[01]: /tstoy/about diff --git a/go.work b/go.work new file mode 100644 index 0000000..bd6d840 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.19 + +use ( + ./app + ./samples/go/resources/first +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..821673a --- /dev/null +++ b/go.work.sum @@ -0,0 +1 @@ +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= diff --git a/samples/go/.markdownlint.yaml b/samples/go/.markdownlint.yaml new file mode 100644 index 0000000..26812c9 --- /dev/null +++ b/samples/go/.markdownlint.yaml @@ -0,0 +1,7 @@ +# About topics have different line length requirements; their text is +# automatically wrapped at 80 characters, so it's preferable to control +# the wrapping here instead. +extends: ../../.markdownlint.json +MD010: + code_blocks: false + spaces_per_tab: 2 diff --git a/samples/go/resources/first/.gitignore b/samples/go/resources/first/.gitignore new file mode 100644 index 0000000..cf77990 --- /dev/null +++ b/samples/go/resources/first/.gitignore @@ -0,0 +1,3 @@ +dist/ +gotstoy +gotstoy.exe diff --git a/samples/go/resources/first/.goreleaser.yaml b/samples/go/resources/first/.goreleaser.yaml new file mode 100644 index 0000000..a4163f5 --- /dev/null +++ b/samples/go/resources/first/.goreleaser.yaml @@ -0,0 +1,52 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +project_name: gotstoy +before: + hooks: + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + ldflags: + - -s -w + # - -X github.com/PowerShell/DSC/docs/examples/gotstoy/cmd.version={{.Version}} + # - -X github.com/PowerShell/DSC/docs/examples/gotstoy/cmd.commit={{.ShortCommit}} + # - -X github.com/PowerShell/DSC/docs/examples/gotstoy/cmd.date={{.CommitDate}} + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + # Add the resource manifest to the archive + files: + - src: gotstoy.dsc.resource.json + strip_parent: true + rlcp: true +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/samples/go/resources/first/LICENSE b/samples/go/resources/first/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/samples/go/resources/first/README.md b/samples/go/resources/first/README.md new file mode 100644 index 0000000..45c86fd --- /dev/null +++ b/samples/go/resources/first/README.md @@ -0,0 +1,419 @@ +# Write your first DSC Resource in Go sample code + +This folder contains the sample code for the completed _Write your first DSC Resource_ tutorial +in Go, as well as the tutorial's documentation files. + +## Building the sample + +You can build the sample code with the included PowerShell build script, or running the commands +manually. The build script handles building the DSC Resource, adding it to the path, and +registering shell completions. + +To manually build the sample for the current operating system, navigate to this folder. Then, run +the following commands: + +- On Linux or macOS: + + ```sh + go build -o gotstoy . + export PATH=$(pwd):$PATH + ``` + +- On Windows: + + ```powershell + go build -o gotstoy.exe . + $env:Path = $PWD.Path + ';' + $env:Path + ``` + +## Getting current state + +You can retrieve the current state of the resource with the `get` command. + +```sh +# Get current state with flags +gotstoy get --scope machine --ensure present --updateAutomatically=false +``` + +```json +{"ensure":"absent","scope":"machine"} +``` + +```sh +# Get with JSON over stdin +' +{ + "scope": "user", + "ensure": "present", + "updateAutomatically": true, + "updateFrequency": 45 +} +' | gotstoy get +``` + +```json +{"ensure":"absent","scope":"user"} +``` + +```sh +# Get current state of all scopes: +gotstoy get --all +``` + +```json +{"ensure":"absent","scope":"machine"} +{"ensure":"absent","scope":"user"} +``` + +## Setting desired state + +You can enforce the state of the resource with the `set` command. + +```sh +# Set the state with flags +gotstoy set --scope machine --ensure present --updateAutomatically=false +# Set with JSON over stdin +' +{ + "scope": "user", + "ensure": "present", + "updateAutomatically": true, + "updateFrequency": 45 +} +' | gotstoy set +# Get new state of all scopes: +gotstoy get --all +``` + +```json +{"ensure":"present","scope":"machine","updateAutomatically":false} + +{"ensure":"present","scope":"user","updateAutomatically":true,"updateFrequency":45} + +{"ensure":"present","scope":"machine","updateAutomatically":false} +{"ensure":"present","scope":"user","updateAutomatically":true,"updateFrequency":45} +``` + +## Verifying state + +After you've enforced state, you should verify the changes with the `tstoy` application itself: + +```sh +tstoy show +``` + +```yaml +Default configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } +} +Machine configuration: { + "updates": { + "automatic": false + } +} +User configuration: { + "updates": { + "automatic": true, + "checkfrequency": 45 + } +} +Final configuration: { + "updates": { + "automatic": true, + "checkfrequency": 45 + } +} +``` + +## Using `dsc resource` + +You can list `gotstoy` as a DSC Resource to inspect it: + +```pwsh +dsc resource list TSToy.Example/gotstoy +``` + +```yaml +type: TSToy.Example/gotstoy +version: '' +path: C:\dsc-samples\samples\go\resources\first\dist\gotstoy_windows_amd64_v1\gotstoy.dsc.resource.json +directory: C:\dsc-samples\samples\go\resources\first\dist\gotstoy_windows_amd64_v1 +implementedAs: Command +author: null +properties: [] +requires: null +manifest: + manifestVersion: '1.0' + type: TSToy.Example/gotstoy + version: 0.1.0 + description: The go implementation of a DSC Resource for the fictional TSToy application. + get: + executable: gotstoy + args: + - get + input: stdin + set: + executable: gotstoy + args: + - set + input: stdin + preTest: true + return: state + schema: + embedded: + $schema: https://json-schema.org/draft/2020-12/schema + title: Golang TSToy Resource + type: object + required: + - scope + properties: + ensure: + title: Ensure configuration file existence + description: Defines whether the application's configuration file for this scope should exist or not. + type: string + enum: + - present + - absent + default: present + scope: + title: Target configuration scope + description: Defines which of the application's configuration files the resource should manage. + type: string + enum: + - machine + - user + updateAutomatically: + title: Should update automatically + description: Indicates whether the application should check for updates when it starts. + type: boolean + updateFrequency: + title: Update check frequency + description: Indicates how many days the application should wait before checking for updates. + type: integer + minimum: 1 + maximum: 90 +``` + +You can retrieve the current state of the resource: + +```powershell +$ResourceName = 'TSToy.Example/gotstoy' +$MachineSettings = '{ "scope": "machine", "ensure": "present", "updateAutomatically": false }' +$UserSettings = @' +{ + "scope": "user", + "ensure": "present", + "updateAutomatically": true, + "updateFrequency": 45 +} +'@ +# Get current state with flags +dsc resource get --resource $ResourceName --input $MachineSettings +# Get with JSON over stdin +$UserSettings | dsc resource get --resource $ResourceName +``` + +```yaml +actual_state: + ensure: absent + scope: machine +``` + +```yaml +actual_state: + ensure: absent + scope: user +``` + +You can test whether the resource is in the desired state: + +```powershell +# Test current state with flags +dsc resource test --resource $ResourceName --input $MachineSettings +# Test with JSON over stdin +$UserSettings | dsc resource test --resource $ResourceName +``` + +```yaml +expected_state: + scope: machine + ensure: present + updateAutomatically: false +actual_state: + ensure: absent + scope: machine +diff_properties: +- ensure +- updateAutomatically +``` + +```yaml +expected_state: + scope: user + ensure: present + updateAutomatically: true + updateFrequency: 45 +actual_state: + ensure: absent + scope: user +diff_properties: +- ensure +- updateAutomatically +- updateFrequency +``` + +You can enforce the desired state for the resource: + +```powershell +# Set desired state with flags +dsc resource set --resource $ResourceName --input $MachineSettings +# Set with JSON over stdin +$UserSettings | dsc resource set --resource $ResourceName +``` + +```yaml +before_state: + ensure: absent + scope: machine +after_state: + ensure: present + scope: machine + updateAutomatically: false +changed_properties: +- ensure +- updateAutomatically +``` + +```yaml +before_state: + ensure: absent + scope: user +after_state: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 +changed_properties: +- ensure +- updateAutomatically +- updateFrequency +``` + +And when you call it again, the output shows that the resource didn't change the settings: + +```powershell +# Set desired state with flags +dsc resource set --resource $ResourceName --input $MachineSettings +# Set with JSON over stdin +$UserSettings | dsc resource set --resource $ResourceName +``` + +```yaml +before_state: + ensure: present + scope: machine + updateAutomatically: false +after_state: + ensure: present + scope: machine + updateAutomatically: false +changed_properties: [] +``` + +```yaml +before_state: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 +after_state: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 +changed_properties: [] +``` + +Finally, you can call `get` and `test` again: + +```powershell +# Set desired state with flags +dsc resource get --resource $ResourceName --input $MachineSettings +# Set with JSON over stdin +$UserSettings | dsc resource test --resource $ResourceName +``` + +```yaml +actual_state: + ensure: present + scope: machine + updateAutomatically: false +``` + +```yaml +expected_state: + scope: user + ensure: present + updateAutomatically: true + updateFrequency: 45 +actual_state: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 +diff_properties: [] +``` + +## Using `dsc config` + +Save this configuration to a file or variable. It sets the configuration files for the application +using the go implementation. To use a different implementation, replace the value for the `type` +key in the resource entries. + +```yaml +$schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json +resources: +- name: All Users Configuration + type: TSToy.Example/gotstoy + properties: + scope: machine + ensure: present + updateAutomatically: false +- name: Current User Configuration + type: TSToy.Example/gotstoy + properties: + scope: user + ensure: present + updateAutomatically: true + updateFrequency: 45 +``` + +Get the current state of the resources with `dsc config get`: + +```powershell +$config | dsc config get +``` + +```yaml +results: +- name: All Users Configuration + type: TSToy.Example/gotstoy + result: + actual_state: + ensure: present + scope: machine + updateAutomatically: false +- name: Current User Configuration + type: TSToy.Example/gotstoy + result: + actual_state: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 +messages: [] +hadErrors: false +``` diff --git a/samples/go/resources/first/build.ps1 b/samples/go/resources/first/build.ps1 new file mode 100644 index 0000000..ff24b75 --- /dev/null +++ b/samples/go/resources/first/build.ps1 @@ -0,0 +1,104 @@ +#!/usr/bin/env pwsh + +<# + .SYNOPSIS + Builds the gotstoy DSC Resource. + + .DESCRIPTION + This build script handles building the `gotstoy` DSC Resource. You can use it to build the DSC + Resource for your current platform, for every platform, or to run the acceptance tests. + + .PARAMETER Target + Defines the target to build. Valid values are `build`, `package`, and `test`. The default value + is `build`. + + When the value is `build`, the script builds the DSC Resource for the current platform. When + the value is `package`, the script builds the DSC Resource for every platform. When the value + is `test`, the script builds the DSC Resource for the current platform and runs the acceptance + tests. + + .PARAMETER Initialize + When specified, the script adds the DSC Resource to the current PowerShell session as an alias + and registers the command's completions. + + .PARAMETER AddToPath + When specified, the script adds the DSC Resource to the current PowerShell session's PATH + environment variable. DSC won't recognize the built application unless it's in the PATH. +#> + +[CmdletBinding()] +param ( + [ValidateSet('build', 'package', 'test')] + [string]$Target = 'build', + [switch]$Initialize, + [switch]$AddToPath +) + +function Build-Project { + [cmdletbinding()] + [OutputType([System.Management.Automation.ApplicationInfo])] + param( + [switch]$All + ) + + begin { + $Arguments = @() + } + + process { + if ($All) { + $Arguments = @( + 'release' + '--skip-publish' + '--skip-announce' + '--skip-validate' + '--clean' + '--release-notes', './RELEASE_NOTES.md' + ) + } else { + $Arguments = @( + 'build' + '--snapshot' + '--clean' + '--single-target' + ) + } + + & goreleaser @Arguments + + Get-Command './dist/gotstoy*/gotstoy*' -ErrorAction Stop + } +} + +switch ($Target) { + 'build' { + $Application = Build-Project -ErrorAction Stop + $ApplicationFolder = Split-Path -Parent $Application.Path + Copy-Item -Path "$PSScriptRoot/gotstoy.dsc.resource.json" -Destination $ApplicationFolder + if ($AddToPath) { + $PathSeparator = [System.IO.Path]::PathSeparator + if ($ApplicationFolder -notin ($env:PATH -split $PathSeparator)) { + $env:PATH = $ApplicationFolder + $PathSeparator + $env:PATH + } + } + if ($Initialize) { + $Alias = Set-Alias -Name gotstoy -Value $Application.Path -PassThru + Invoke-Expression $(gotstoy completion powershell | Out-String) + $Alias + } else { + $Application + } + } + 'package' { + Build-Project -All -ErrorAction Stop + } + 'test' { + $Application = Build-Project -ErrorAction Stop + $ApplicationFolder = Split-Path -Parent $Application.Path + Copy-Item -Path "$PSScriptRoot/gotstoy.dsc.resource.json" -Destination $ApplicationFolder + $TestContainer = New-PesterContainer -Path 'acceptance.tests.ps1' -Data @{ + Application = $Application + } + Invoke-Pester -Container $TestContainer -Output Detailed + } +} \ No newline at end of file diff --git a/samples/go/resources/first/cmd/get.go b/samples/go/resources/first/cmd/get.go new file mode 100644 index 0000000..1da258a --- /dev/null +++ b/samples/go/resources/first/cmd/get.go @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + + "github.com/PowerShell/DSC-Samples/go/resources/first/config" + "github.com/spf13/cobra" +) + +var all bool + +// getCmd represents the get command +var getCmd = &cobra.Command{ + Use: "get", + Short: "Gets the current state of a tstoy configuration file.", + Long: `The get command returns the current state of a tstoy configuration +file as a JSON blob to stdout.`, + RunE: getState, +} + +func init() { + rootCmd.AddCommand(getCmd) + getCmd.Flags().BoolVar( + &all, + "all", + false, + "Get the configurations for all scopes.", + ) +} + +func getState(cmd *cobra.Command, args []string) error { + list := []config.Settings{} + if all { + list = append( + list, + config.Settings{Scope: config.ScopeMachine}, + config.Settings{Scope: config.ScopeUser}, + ) + } else if targetScope != config.ScopeUndefined { + // explicit --scope overrides JSON + list = append(list, config.Settings{Scope: targetScope}) + } else if inputJSON != nil { + list = append(list, *inputJSON) + } else { + // fails but with consistent messaging + list = append(list, config.Settings{Scope: targetScope}) + } + + for _, s := range list { + + err := s.Validate() + if err != nil { + return fmt.Errorf("can't get settings; %s", err) + } + + config, err := s.GetConfigSettings() + if err != nil { + return fmt.Errorf("failed to get settings; %s", err) + } + + err = config.Print() + if err != nil { + return err + } + } + + return nil +} diff --git a/samples/go/resources/first/cmd/root.go b/samples/go/resources/first/cmd/root.go new file mode 100644 index 0000000..92b37e5 --- /dev/null +++ b/samples/go/resources/first/cmd/root.go @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cmd + +import ( + "os" + + "github.com/PowerShell/DSC-Samples/go/resources/first/config" + "github.com/PowerShell/DSC-Samples/go/resources/first/input" + "github.com/spf13/cobra" + "github.com/thediveo/enumflag" +) + +var targetScope config.Scope +var targetEnsure config.Ensure +var updateAutomatically bool +var updateFrequency config.Frequency +var inputJSON *config.Settings + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gotstoy", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Unlike normal cobra apps, this one sets the args explicitly from main to +// account for JSON blobs sent from stdin. +func Execute(args []string) { + rootCmd.SetArgs(args) + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().Var( + enumflag.New(&targetScope, "scope", config.ScopeMap, enumflag.EnumCaseInsensitive), + "scope", + "The target scope for the configuration.", + ) + rootCmd.RegisterFlagCompletionFunc("scope", config.ScopeFlagCompletion) + + rootCmd.PersistentFlags().Var( + enumflag.New(&targetEnsure, "ensure", config.EnsureMap, enumflag.EnumCaseInsensitive), + "ensure", + "Whether the configuration file should exist.", + ) + rootCmd.RegisterFlagCompletionFunc("ensure", config.EnsureFlagCompletion) + + rootCmd.PersistentFlags().BoolVar( + &updateAutomatically, + "updateAutomatically", + false, + "Whether the configuration should set the app to automatically update.", + ) + + rootCmd.PersistentFlags().Var( + &updateFrequency, + "updateFrequency", + "How frequently the configuration should update, between 1 and 90 days inclusive.", + ) + + rootCmd.PersistentFlags().Var( + &input.JSONFlag{Target: &inputJSON}, + "inputJSON", + "Specify options as a JSON blob instead of using the scope, ensure, and update* flags.", + ) +} diff --git a/samples/go/resources/first/cmd/set.go b/samples/go/resources/first/cmd/set.go new file mode 100644 index 0000000..47d1e00 --- /dev/null +++ b/samples/go/resources/first/cmd/set.go @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + + "github.com/PowerShell/DSC-Samples/go/resources/first/config" + "github.com/spf13/cobra" +) + +// setCmd represents the set command +var setCmd = &cobra.Command{ + Use: "set", + Short: "Sets a tstoy configuration file to the desired state.", + Long: `The set command ensures that the tstoy configuration file for a +specific scope has the desired settings. It returns the updated settings state +as a JSON blob to stdout.`, + RunE: setState, +} + +func init() { + rootCmd.AddCommand(setCmd) +} + +func setState(cmd *cobra.Command, args []string) error { + enforcing := config.Settings{} + + if inputJSON != nil { + enforcing = *inputJSON + } + if targetScope != config.ScopeUndefined { + enforcing.Scope = targetScope + } + if targetEnsure != config.EnsureUndefined { + enforcing.Ensure = targetEnsure + } + if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed { + enforcing.UpdateAutomatically = &updateAutomatically + } + if updateFrequency != 0 { + enforcing.UpdateFrequency = updateFrequency + } + + final, err := enforcing.Enforce() + if err != nil { + return fmt.Errorf("can't enforce settings; %s", err) + } + + return final.Print() +} diff --git a/samples/go/resources/first/config/config.go b/samples/go/resources/first/config/config.go new file mode 100644 index 0000000..c89957a --- /dev/null +++ b/samples/go/resources/first/config/config.go @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/knadh/koanf/maps" + "github.com/spf13/cobra" +) + +type Settings struct { + Ensure Ensure `json:"ensure,omitempty"` + Scope Scope `json:"scope,omitempty"` + UpdateAutomatically *bool `json:"updateAutomatically,omitempty"` + UpdateFrequency Frequency `json:"updateFrequency,omitempty"` + configPath string +} + +func (s *Settings) GetConfigPath() (string, error) { + if s.configPath == "" { + path, err := getAppConfigPath(s.Scope) + if err != nil { + return "", err + } + s.configPath = path + } + + return s.configPath, nil +} + +func (s *Settings) GetConfigMap() (map[string]any, error) { + path, err := s.GetConfigPath() + if err != nil { + return nil, err + } + return getAppConfigMap(path) +} + +func (s *Settings) GetConfigSettings() (Settings, error) { + config, err := s.GetConfigMap() + if errors.Is(err, os.ErrNotExist) { + return Settings{ + Ensure: EnsureAbsent, + Scope: s.Scope, + }, nil + } else if err != nil { + return Settings{}, err + } + + return getAppConfigSettings(s.Scope, config) +} + +func (s *Settings) Enforce() (*Settings, error) { + err := s.Validate() + if err != nil { + return nil, err + } + + current, err := s.GetConfigSettings() + if err != nil { + return nil, err + } + + if s.Ensure == EnsureAbsent { + return s.remove(current) + } + + if current.Ensure == EnsureAbsent { + return s.create(current) + } + + return s.update(current) +} + +func (s *Settings) create(currentSettings Settings) (*Settings, error) { + configDir := filepath.Dir(s.configPath) + if err := os.MkdirAll(configDir, 0750); err != nil { + return ¤tSettings, fmt.Errorf( + "failed to create folder for config file in '%s': %s", + configDir, + err, + ) + } + configFile, err := os.Create(s.configPath) + if err != nil { + return ¤tSettings, fmt.Errorf( + "failed to create config file '%s': %s", + s.configPath, + err, + ) + } + + // Create the JSON for the tstoy configuration file. + // Can't just marshal the Settings instance because it's a representation + // of the settings, not a literal blob of the settings. + settings := make(map[string]any) + updates := make(map[string]any) + addUpdates := false + if s.UpdateAutomatically != nil { + addUpdates = true + updates["automatic"] = *s.UpdateAutomatically + } + if s.UpdateFrequency != 0 { + addUpdates = true + updates["checkFrequency"] = s.UpdateFrequency + } + if addUpdates { + settings["updates"] = updates + } + + configJSON, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return ¤tSettings, fmt.Errorf( + "unable to convert settings to json: %s", + err, + ) + } + + _, err = configFile.Write(configJSON) + if err != nil { + return ¤tSettings, fmt.Errorf( + "unable to write config file: %s", + err, + ) + } + + return s, nil +} + +func (s *Settings) update(current Settings) (*Settings, error) { + writeConfig := false + + currentMap, err := current.GetConfigMap() + if err != nil { + return nil, err + } + + // ensure the map keys are all strings. + maps.IntfaceKeysToStrings(currentMap) + + // Check for the update settings + updates, ok := currentMap["updates"] + if !ok { + currentMap["updates"] = make(map[string]any) + updates = currentMap["updates"] + } + + // Only update if desired state defines UpdateAutomatically and: + // 1. Current state doesn't define it, or + // 2. Current state's setting doesn't match desired state. + shouldSetUA := false + if s.UpdateAutomatically != nil { + if current.UpdateAutomatically == nil { + shouldSetUA = true + } else if *s.UpdateAutomatically != *current.UpdateAutomatically { + shouldSetUA = true + } + } + + if shouldSetUA { + writeConfig = true + updates.(map[string]any)["automatic"] = *s.UpdateAutomatically + } else if current.UpdateAutomatically != nil { + updates.(map[string]any)["automatic"] = *current.UpdateAutomatically + } + + // Only update if desired state defines UpdateFrequency and: + // 1. Current state doesn't define it, or + // 2. Current state's setting doesn't match desired state. + if s.UpdateFrequency != 0 && s.UpdateFrequency != current.UpdateFrequency { + writeConfig = true + updates.(map[string]any)["checkFrequency"] = s.UpdateFrequency + } else if current.UpdateFrequency != 0 { + updates.(map[string]any)["checkFrequency"] = current.UpdateFrequency + } + + // no changes made, leave config untouched + if !writeConfig { + return s, nil + } + + currentMap["updates"] = updates.(map[string]any) + + configJson, err := json.MarshalIndent(currentMap, "", " ") + if err != nil { + return ¤t, fmt.Errorf( + "unable to convert updated settings to json: %s", + err, + ) + } + + err = os.WriteFile(s.configPath, configJson, 0750) + if err != nil { + return ¤t, fmt.Errorf( + "unable to write updated config file: %s", + err, + ) + } + + return s, nil +} + +func (s *Settings) remove(current Settings) (*Settings, error) { + if current.Ensure == EnsureAbsent { + return s, nil + } + + // At this point, s.GetConfigPath() has already run without an error, + // so we can rely on accessing the private field directly. + err := os.Remove(s.configPath) + if err != nil { + return ¤t, err + } + + return s, nil +} + +func (s *Settings) Print() error { + configJson, err := json.Marshal(s) + if err != nil { + return err + } + + fmt.Println(string(configJson)) + + return nil +} + +type Ensure int + +const ( + EnsureUndefined Ensure = iota + EnsureAbsent + EnsurePresent +) + +func (e Ensure) String() string { + switch e { + case EnsureAbsent: + return "absent" + case EnsurePresent: + return "present" + } + + return "undefined" +} + +func ParseEnsure(s string) (Ensure, error) { + switch strings.ToLower(s) { + case "absent": + return EnsureAbsent, nil + case "present": + return EnsurePresent, nil + } + + return EnsureUndefined, fmt.Errorf( + "unable to convert '%s' to Ensure, must be one of: absent, present", + s, + ) +} + +func (e Ensure) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} + +func (ensure *Ensure) UnmarshalJSON(data []byte) (err error) { + var e string + if err := json.Unmarshal(data, &e); err != nil { + return err + } + if *ensure, err = ParseEnsure(e); err != nil { + return err + } + + return nil +} + +var EnsureMap = map[Ensure][]string{ + EnsureAbsent: {"absent"}, + EnsurePresent: {"present"}, +} + +func EnsureFlagCompletion( + cmd *cobra.Command, + args []string, + toComplete string, +) ([]string, cobra.ShellCompDirective) { + completions := []string{ + "absent\tThe configuration file shouldn't exist.", + "present\tThe configuration file should exist.", + } + return completions, cobra.ShellCompDirectiveNoFileComp +} + +type Scope int + +const ( + ScopeUndefined Scope = iota + ScopeMachine + ScopeUser +) + +func (s Scope) String() string { + switch s { + case ScopeMachine: + return "machine" + case ScopeUser: + return "user" + } + + return "undefined" +} + +func ParseScope(s string) (Scope, error) { + switch strings.ToLower(s) { + case "machine": + return ScopeMachine, nil + case "user": + return ScopeUser, nil + } + + return ScopeUndefined, fmt.Errorf( + "unable to convert '%s' to Scope, must be one of: machine, user", + s, + ) +} + +func (s Scope) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (scope *Scope) UnmarshalJSON(data []byte) (err error) { + var e string + if err := json.Unmarshal(data, &e); err != nil { + return err + } + if *scope, err = ParseScope(e); err != nil { + return err + } + + return nil +} + +var ScopeMap = map[Scope][]string{ + ScopeMachine: {"machine"}, + ScopeUser: {"user"}, +} + +func ScopeFlagCompletion( + cmd *cobra.Command, + args []string, + toComplete string, +) ([]string, cobra.ShellCompDirective) { + completions := []string{ + "machine\tThe configuration file should exist.", + "user\tThe configuration file shouldn't exist.", + } + return completions, cobra.ShellCompDirectiveNoFileComp +} + +type Frequency int + +func (f Frequency) Validate() error { + v := int(f) + if v < 1 || v > 90 { + return fmt.Errorf( + "invalid value %v; must be an integer between 1 and 90, inclusive", + v, + ) + } + return nil +} + +func (f *Frequency) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *f = Frequency(v) + + return f.Validate() +} + +func (f *Frequency) Type() string { + return "int" +} + +func (f *Frequency) String() string { + return strconv.Itoa(int(*f)) +} + +func (s *Settings) Validate() error { + if s.Scope == ScopeUndefined { + return fmt.Errorf( + "the Scope setting isn't defined. Must define a Scope for Settings", + ) + } + + if s.Ensure == EnsureAbsent { + return nil + } + + if s.UpdateFrequency != 0 { + return s.UpdateFrequency.Validate() + } + + return nil +} + +func getAppConfigPath(s Scope) (string, error) { + args := []string{"show", "path", s.String()} + + output, err := exec.Command("tstoy", args...).Output() + if err != nil { + return "", err + } + + // We need to trim trailing whitespace automatically emitted for the path. + path := string(output) + path = strings.Trim(path, "\n") + path = strings.Trim(path, "\r") + + return path, nil +} + +func getAppConfigMap(path string) (map[string]any, error) { + var config map[string]any + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, &config) + return config, err +} + +func getAppConfigSettings(scope Scope, config map[string]any) (Settings, error) { + // ensure the map keys are all strings. + maps.IntfaceKeysToStrings(config) + + // Since we found the config, we know the scope and ensure state. + settings := Settings{ + Scope: scope, + Ensure: EnsurePresent, + } + + // Check for the update settings + updates, ok := config["updates"] + if ok { + for key, value := range updates.(map[string]any) { + switch key { + case "automatic": + auto := value.(bool) + settings.UpdateAutomatically = &auto + case "checkFrequency": + intValue := int(value.(float64)) + frequency := Frequency(intValue) + settings.UpdateFrequency = frequency + } + } + } + + return settings, nil +} diff --git a/samples/go/resources/first/docs/1-create.md b/samples/go/resources/first/docs/1-create.md new file mode 100644 index 0000000..a51ea2e --- /dev/null +++ b/samples/go/resources/first/docs/1-create.md @@ -0,0 +1,86 @@ +--- +title: Step 1 - Create the DSC Resource +weight: 1 +dscs: + menu_title: 1. Create the resource +--- + +Create a new folder called `gotstoy` and open it in VS Code. This folder is the root folder for the +project. + +```sh +mkdir ./gotstoy +code ./gotstoy +``` + +Open the integrated terminal in VS Code. In that terminal, initialize the folder as a Go module. + +```sh +go mod init "github.com//gotstoy" +``` + +In this tutorial, you'll be creating a DSC Resource with [Cobra][01]. Cobra helps you create a +command line application in Go. It handles argument parsing, setting flags, shell completions, and +help. + +Use the following command to install `cobra-cli`. + +```sh +go install github.com/spf13/cobra-cli@latest +``` + +Use `cobra-cli` to scaffold the DSC Resource application and add the `get` and `set` commands. + +```sh +cobra-cli init +cobra-cli add get +cobra-cli add set +``` + +Run the following commands to get the Go modules you'll be using outside of the standard library. + +```sh +go get github.com/thediveo/enumflag@v0.10.1 +go get github.com/TylerBrock/colorjson@v0.0.0-20200706003622-8a50f05110d2 +go get github.com/knadh/koanf/maps@v0.1.1 +``` + +The `enumflag` module simplifies using enumerations as command line flags. The `colorjson` module +enables you to pretty-print output in the console. The `maps` module makes interacting with +arbitrary maps easier. + +Verify that the new application can run and has the expected commands. + +```sh +go run ./main.go +``` + +```text +A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application. + +Usage: + gotstoy [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + get A brief description of your command + help Help about any command + set A brief description of your command + +Flags: + -h, --help help for gotstoy + -t, --toggle Help message for toggle + +Use "gotstoy [command] --help" for more information about a command. +``` + +With the command scaffolded, you need to understand the application the DSC Resource manages before +you can implement the commands. By now, you should have read [About the TSToy application][02]. + +[01]: https://cobra.dev/ +[02]: /tstoy/about diff --git a/samples/go/resources/first/docs/2-define-config-settings.md b/samples/go/resources/first/docs/2-define-config-settings.md new file mode 100644 index 0000000..c04ddd2 --- /dev/null +++ b/samples/go/resources/first/docs/2-define-config-settings.md @@ -0,0 +1,404 @@ +--- +title: Step 2 - Define the configuration settings +weight: 2 +dscs: + menu_title: 2. Define settings +--- + +## Create the config module + +Create the `config` folder in the project root. Inside it, create the `config.go` file. This file +defines the configuration types and functions of the DSC Resource. + +```sh +mkdir ./config +touch ./config/config.go +``` + +Set the package name at the top of the file to `config`: + +```go +package config +``` + +Create a new type called `Settings` as a struct. The fields of the struct are the settings the DSC +Resource manages. In the struct, define the `Ensure`, `Scope`, `UpdateAutomatically`, and +`UpdateFrequency` fields. Set their types to `any`. + +```go +type Settings struct { + Ensure any + Scope any + UpdateAutomatically any + UpdateFrequency any +} +``` + +Defining the fields this way is convenient but also inaccurate. To make the DSC Resource more +reliable, you need to define the fields to match their purpose. + +## Define Ensure + +The `Ensure` field follows a common pattern in DSC for managing whether an instance of a DSC +Resource should exist. To use this pattern, the field should be an enumeration with the valid +values `absent` and `present`. + +Create a new type called `Ensure` as an integer. + +```go +type Ensure int +``` + +Define three constants of the `Ensure` type to use as enumerations: `EnsureUndefined`, +`EnsureAbsent`, and `EnsurePresent`. + +```go +const ( + EnsureUndefined Ensure = iota + EnsureAbsent + EnsurePresent +) +``` + +Implement the [fmt.Stringer][01] interface for the `Ensure` type. This interface translates the +values into strings. It should return `"absent"`, `"present"`, or `"undefined"`. + +```go +func (e Ensure) String() string { + switch e { + case EnsureAbsent: + return "absent" + case EnsurePresent: + return "present" + } + + return "undefined" +} +``` + +Implement a function called `ParseEnsure` that converts an input string into an `Ensure` enum and +returns an error if the input can't be parsed as `EnsurePresent` or `EnsureAbsent`. + +```go +func ParseEnsure(s string) (Ensure, error) { + switch strings.ToLower(s) { + case "absent": + return EnsureAbsent, nil + case "present": + return EnsurePresent, nil + } + + return EnsureUndefined, fmt.Errorf( + "unable to convert '%s' to Ensure, must be one of: absent, present", + s, + ) +} +``` + +Implement the `MarshalJSON` and `UnmarshalJSON` methods for `Ensure` that convert to and from JSON +as the enum's label instead of the integer value. + +```go +func (e Ensure) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} + +func (ensure *Ensure) UnmarshalJSON(data []byte) (err error) { + var e string + if err := json.Unmarshal(data, &e); err != nil { + return err + } + if *ensure, err = ParseEnsure(e); err != nil { + return err + } + + return nil +} +``` + +Create a variable called `EnsureMap` to map the enumeration value to its string. This map is used +when you define the command line flags for the DSC Resource. + +```go +var EnsureMap = map[Ensure][]string{ + EnsureAbsent: {"absent"}, + EnsurePresent: {"present"}, +} +``` + +Create a function called `EnsureFlagCompletion`. This function provides shell completion for the +command-line flags of the DSC Resource. + +```go +func EnsureFlagCompletion( + cmd *cobra.Command, + args []string, + toComplete string, +) ([]string, cobra.ShellCompDirective) { + completions := []string{ + "absent\tThe configuration file shouldn't exist.", + "present\tThe configuration file should exist.", + } + return completions, cobra.ShellCompDirectiveNoFileComp +} +``` + +Update the `Ensure` field of the `Settings` type to use the newly defined `Ensure` value instead of +`any`. + +```go +type Settings struct { + Ensure Ensure + Scope any + UpdateAutomatically any + UpdateFrequency any +} +``` + +## Define Scope + +The `Scope` field of the `Settings` struct defines which instance of the `tstoy` configuration file +the DSC Resource should manage. Like `Ensure`, it should be an enumeration. + +Define the `Scope` as an integer. Add constant values for the enumeration as `ScopeUndefined`, +`ScopeMachine`, and `ScopeUser`. + +```go +type Scope int + +const ( + ScopeUndefined Scope = iota + ScopeMachine + ScopeUser +) +``` + +`Scope` needs the same functions and methods you defined for `Ensure`, but for its own enumeration +values. + +```go +func (s Scope) String() string { + switch s { + case ScopeMachine: + return "machine" + case ScopeUser: + return "user" + } + + return "undefined" +} + +func ParseScope(s string) (Scope, error) { + switch strings.ToLower(s) { + case "machine": + return ScopeMachine, nil + case "user": + return ScopeUser, nil + } + + return ScopeUndefined, fmt.Errorf( + "unable to convert '%s' to Scope, must be one of: machine, user", + s, + ) +} + +func (s Scope) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (scope *Scope) UnmarshalJSON(data []byte) (err error) { + var e string + if err := json.Unmarshal(data, &e); err != nil { + return err + } + if *scope, err = ParseScope(e); err != nil { + return err + } + + return nil +} + +var ScopeMap = map[Scope][]string{ + ScopeMachine: {"machine"}, + ScopeUser: {"user"}, +} + +func ScopeFlagCompletion( + cmd *cobra.Command, + args []string, + toComplete string, +) ([]string, cobra.ShellCompDirective) { + completions := []string{ + "machine\tThe configuration file should exist.", + "user\tThe configuration file shouldn't exist.", + } + return completions, cobra.ShellCompDirectiveNoFileComp +} +``` + +When you've implemented the `Scope` type, enumerations, methods, and functions, update the `Scope` +field of the `Settings` type. + +```go +type Settings struct { + Ensure Ensure + Scope Scope + UpdateAutomatically any + UpdateFrequency any +} +``` + +## Define UpdateAutomatically + +Like `Ensure` and `Scope`, whether the `tstoy` application should be configured for automatic +updates only has two options. Unlike `Ensure` and `Scope`, you can represent those options as a +boolean. + +Update the `UpdateAutomatically` field of the `Settings` type to be a pointer to a boolean value. +Using a pointer for this field allows the value to be `nil`, which enables the DSC Resource to +distinguish between the setting not being specified and being specified as `false`. + +```go +type Settings struct { + Ensure Ensure + Scope Scope + UpdateAutomatically *bool + UpdateFrequency any +} +``` + +If the value wasn't a pointer, the DSC Resource would need extra handling to distinguish between +whether the value is false because the user or configuration file specified the value as `false` or +because it wasn't specified at all. + +## Define UpdateFrequency + +The `UpdateFrequency` field represents a count of days between `1` and `90`, inclusive. To add +validation for the field, define a new type called `Frequency` as an integer. + +```go +type Frequency int +``` + +Next, define the `Validate` method to check whether a `Frequency` value is valid for the setting. +It should return an error when the integer value of the Frequency is out of range. + +```go +func (f Frequency) Validate() error { + v := int(f) + if v < 1 || v > 90 { + return fmt.Errorf( + "invalid value %v; must be an integer between 1 and 90, inclusive", + v, + ) + } + return nil +} +``` + +To make the new type usable as a command line flag, you need to implement the `Set`, `Type`, and +`String` methods of the [pflag.Value interface][02]. + +```go +func (f *Frequency) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *f = Frequency(v) + + return f.Validate() +} + +func (f *Frequency) Type() string { + return "int" +} + +func (f *Frequency) String() string { + return strconv.Itoa(int(*f)) +} +``` + +Finally, update the `UpdateFrequency` field of `Settings` to use the defined `Frequency` type. + +```go +type Settings struct { + Ensure Ensure + Scope Scope + UpdateAutomatically *bool + UpdateFrequency Frequency +} +``` + +## Ensure Settings serializes to JSON correctly { toc_text="Serialize Settings to JSON" } + +Now that the `Settings` type is defined and has the correct value types for each field, you need to +add tags to the fields so they can be marshalled to and unmarshalled from JSON correctly. + +```go +type Settings struct { + Ensure Ensure `json:"ensure,omitempty"` + Scope Scope `json:"scope,omitempty"` + UpdateAutomatically *bool `json:"updateAutomatically,omitempty"` + UpdateFrequency Frequency `json:"updateFrequency,omitempty"` +} +``` + +The tags should all be in the format `json:",omitempty"`. The first value defines the +name of the key that the DSC Resource expects from JSON input and uses for returning an instance of +`Settings` to DSC. The `omitempty` value indicates that if the fields value is the same as its zero +value, that key shouldn't be included in the output JSON. + +## Ensure Settings can print as JSON + +Now that an instance of `Settings` can correctly serialize to JSON, define the `Print()` method to +simplify emitting the instance as a single line of JSON. + +```go +func (s *Settings) Print() error { + configJson, err := json.Marshal(s) + if err != nil { + return err + } + + fmt.Println(string(configJson)) + + return nil +} +``` + +## Implement validation for Settings + +The DSC Resource should be able to report whether an instance of Settings is valid and, if it +isn't, how it's invalid. + +Add the `Validate` method to return an error if the instance is invalid. + +```go +func (s *Settings) Validate() error { + if s.Scope == ScopeUndefined { + return fmt.Errorf( + "the Scope setting isn't defined. Must define a Scope for Settings", + ) + } + + if s.Ensure == EnsureAbsent { + return nil + } + + if s.UpdateFrequency != 0 { + return s.UpdateFrequency.Validate() + } + + return nil +} +``` + +The method returns an error if the `Scope` field is undefined because the resource requires a +specific scope to manage a `tstoy` configuration file. It short-circuits the validation if `Ensure` +is set to absent because all other keys are ignored. Finally, if the `UpdateFrequency` field is +invalid, it returns an error message indicating the issue. + +[01]: https://pkg.go.dev/fmt#Stringer +[02]: https://pkg.go.dev/github.com/spf13/pflag#Value diff --git a/samples/go/resources/first/docs/3-handle-input.md b/samples/go/resources/first/docs/3-handle-input.md new file mode 100644 index 0000000..b8d3390 --- /dev/null +++ b/samples/go/resources/first/docs/3-handle-input.md @@ -0,0 +1,379 @@ +--- +title: Step 3 - Handle input +weight: 3 +dscs: + menu_title: 3. Handle input +--- + +Now that the valid values for settings are defined, the root command of the DSC Resource needs to +handle when users specify those values. + +## Define the flag variables + +Open the `cmd/root.go` file in the editor. + +Add variables for each of the fields of the `Settings` type so users can specify those values at +the command line. + +```go +var targetScope config.Scope +var targetEnsure config.Ensure +var updateAutomatically bool +var updateFrequency config.Frequency +``` + +## Handle arguments as input + +The first input method the resource should support is command line arguments. This makes using the +resource outside of DSC easier, like when iteratively developing the command. It's also the most +common way users expect to call a command directly. + +### Define the persistent flags + +Next, find the `init` function at the bottom of the file. Inside it, define persistent flags so +users can pass the values to both `get` and `set` as arguments. + +```go +func init() { + rootCmd.PersistentFlags().Var( + enumflag.New(&targetScope, "scope", config.ScopeMap, enumflag.EnumCaseInsensitive), + "scope", + "The target scope for the configuration.", + ) + rootCmd.RegisterFlagCompletionFunc("scope", config.ScopeFlagCompletion) + + rootCmd.PersistentFlags().Var( + enumflag.New(&targetEnsure, "ensure", config.EnsureMap, enumflag.EnumCaseInsensitive), + "ensure", + "Whether the configuration file should exist.", + ) + rootCmd.RegisterFlagCompletionFunc("ensure", config.EnsureFlagCompletion) + + rootCmd.PersistentFlags().BoolVar( + &updateAutomatically, + "updateAutomatically", + false, + "Whether the configuration should set the app to automatically update.", + ) + + rootCmd.PersistentFlags().Var( + &updateFrequency, + "updateFrequency", + "How frequently the configuration should update, between 1 and 90 days inclusive.", + ) +} +``` + +Use the `enumflag` module to for the `ensure` and `scope` flags. It handles parsing the user inputs +and converting them to the enumeration values. Use the flag completion functions you defined +earlier to ensure that users can opt into shell completions for those flags. + +### Validate input flags + +With the `Settings` defined and command line flags added to the root command, you can begin +validating that the settings flags work as expected. + +Open the `cmd/get.go` file in the editor. + +At the bottom of the file, create a new `getState` function that takes two parameters, a pointer to +`cobra.Command` and a slice of strings, and returns an error. + +```go +func getState(cmd *cobra.Command, args []string) error { + return nil +} +``` + +Replace the `Run` entry in the `getCmd` variable's definition with the `RunE` field set to the +`getState` function. Update the documentation for the command to be more specific to the DSC +Resource. + +```go +var getCmd = &cobra.Command{ + Use: "get", + Short: "Gets the current state of a tstoy configuration file.", + Long: `The get command returns the current state of a tstoy configuration +file as a JSON blob to stdout.`, + RunE: getState, +} +``` + +Next, update the `getState` function to report the value of any specified flags. You'll replace +this implementation later, but it's useful for validating that the flags work as expected. + +```go +func getState(cmd *cobra.Command, args []string) error { + if targetScope != config.ScopeUndefined { + fmt.Println("Specified --scope as", targetScope) + } + if targetEnsure != config.EnsureUndefined { + fmt.Println("Specified --ensure as", targetEnsure) + } + if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed { + fmt.Println("Specified --updateAutomatically as", updateAutomatically) + } + if updateFrequency != 0 { + fmt.Println("Specified --updateFrequency as", updateFrequency) + } + return nil +} +``` + +Run the DSC Resource with different flags to verify the output. + +```sh +go run ./main.go get --scope machine --ensure absent +go run ./main.go get --updateAutomatically --updateFrequency 45 +go run ./main.go get --updateAutomatically=false --ensure Absent +``` + +```Output +Specified --ensure as absent +Specified --scope as machine + +Specified --updateAutomatically as true +Specified --updateFrequency as 45 + +Specified --ensure as absent +Specified --updateAutomatically as false +``` + +Next, you can test that the arguments are validating correctly: + +```sh +go run ./main.go get --scope 1 +go run ./main.go get --scope incorrect +go run ./main.go get --updateFrequency 100 +``` + +```Output +Error: invalid argument "1" for "--scope" flag: must be 'machine', 'user' + +Error: invalid argument "incorrect" for "--scope" flag: must be 'machine', 'user' + +Error: invalid argument "100" for "--updateFrequency" flag: invalid value 100; +must be an integer between 1 and 90, inclusive +``` + +With validation confirmed, the command can accept command-line arguments. + +## Handle JSON input over stdin + +When command-based DSC Resources are called by `dsc` itself, they may get their input as a JSON +blob over `stdin`. While specifying flags at the command line is useful for testing, it's more +robust for the DSC Resource to support sending input over `stdin`. This also makes it easier for +other integrating tools to interact with the DSC Resource. + +### Add handlers for JSON input + +Create the `input` folder in the project root. Inside it, create the `input.go` file. This file +defines how you handle input from `stdin`. + +```sh +mkdir ./input +touch ./input/input.go +``` + +Open `input/input.go` and set the package name to `input`. + +```go +package input +``` + +Now you need to ensure that the DSC Resource can handle a JSON blob as input along with the other +flags. Implement a new type that satisfies the [pflag.Value][05] interface. + +Define a type called `JSONFlag` as a struct with the `Target` field as the `any` type. + +```go +type JSONFlag struct { + Target any +} +``` + +Implement the `String`, `Set`, and `Type` methods for `JSONFlag`. + +```go +func (f *JSONFlag) String() string { + b, err := json.Marshal(f.Target) + if err != nil { + return "failed to marshal object" + } + return string(b) +} + +func (f *JSONFlag) Set(v string) error { + return json.Unmarshal([]byte(v), f.Target) +} + +func (f *JSONFlag) Type() string { + return "json" +} +``` + +### Add handler for stdin + +Next, the DSC Resource needs a function that can handle reading from `stdin`. The function must +operate on the list of arguments for the DSC Resource. If there's input on `stdin`, it's added to +the list of arguments with the `--inputJSON` flag. + +```go +func HandleStdIn(args []string) []string { + info, _ := os.Stdin.Stat() + if (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice { + // do nothing + } else { + stdin, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + // remove surrounding whitespace + jsonBlob := strings.Trim(string(stdin), "\n") + jsonBlob = strings.Trim(jsonBlob, "\r") + jsonBlob = strings.TrimSpace(jsonBlob) + // only add to arguments if the string is non-empty. + if jsonBlob != "" { + args = append(args, "--inputJSON", jsonBlob) + } + } + + return args +} +``` + +The function doesn't need to validate that the input is valid JSON. Instead, the `JSONFlag` and the +command handle the validation. Implementing the function to append the JSON as an argument also +gives the user the choice to pass a JSON blob as a normal argument. + +### Add inputJSON to the root command { toc_md="Add `inputJSON` flag" } + +Before you can pass a JSON blob to the commands, you must update the root command to accept the +`--inputJson` flag. + +Open `cmd/root.go` and add the `inputJSON` variable with its type as a pointer to +`config.Settings`. + +```go +var inputJSON *config.Settings +``` + +In the `init` function, add a new persistent flag for `--inputJSON`. + +```go +rootCmd.PersistentFlags().Var( + &input.JSONFlag{Target: &inputJSON}, + "inputJSON", + "Specify options as a JSON blob instead of using the scope, ensure, and update* flags.", +) +``` + +The new flag uses the `JSONFlag` type defined in the `input` package and sets the `Target` field to +the `inputJSON` variable. Because that variable has the `Settings` type, when the flag +automatically unmarshals the input, it deserializes the blob to `Settings` for the DSC Resource. + +### Update root command to handle stdin { toc_text "Update root command for stdin" } + +Finally, you must update the `Execute` function to take a list of arguments, because argument +passing must be explicitly handled to support `stdin`. + +```go +// Unlike normal cobra apps, this one sets the args explicitly from main to +// account for JSON blobs sent from stdin. +func Execute(args []string) { + rootCmd.SetArgs(args) + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} +``` + +### Update main to handle `stdin` + +Now that the `HandleStdIn` function is defined and the root command is updated, `main` needs to be +updated to pass the arguments to the `Execute` function and handle JSON over `stdin`. + +Open `main.go` and replace the `main` function. + +```go +func main() { + args := []string{} + for index, arg := range os.Args { + // skip the first index, because it's the application name + if index > 0 { + args = append(args, arg) + } + } + + // Check stdin and add any found JSON blob after an --inputJSON flag. + args = input.HandleStdIn(args) + + // execute with the combined arguments + cmd.Execute(args) +} +``` + +## Verify using JSON as input + +Now that the DSC Resource can accept JSON as input over `stdin` or as an argument, the `get` +command needs to handle that input. + +Open `cmd/get.go` and ensure that the `getState` function prints the value for the `inputJSON` +variable if it's not null. + +```go +func getState(cmd *cobra.Command, args []string) error { + if inputJSON != nil { + fmt.Println("Specified inputJSON as:") + (*inputJSON).Print() + } + if targetScope != config.ScopeUndefined { + fmt.Println("Specified --scope as", targetScope) + } + if targetEnsure != config.EnsureUndefined { + fmt.Println("Specified --ensure as", targetEnsure) + } + if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed { + fmt.Println("Specified --updateAutomatically as", updateAutomatically) + } + if updateFrequency != 0 { + fmt.Println("Specified --updateFrequency as", updateFrequency) + } + return nil +} +``` + +You can verify the behavior with a few commands: + +```sh +go run ./main.go get --inputJSON '{ "scope": "machine" }' +go run ./main.go get --inputJSON '{ "scope": "machine" }' --scope user +'{ "scope": "machine" }' | go run ./main.go get +'{ "scope": "machine" }' | go run ./main.go get --scope user +'{ "ensure": "present" }' | go run ./main.go get +``` + +```Output +Specified inputJSON as: +{"scope":"machine"} + +Specified inputJSON as: +{"scope":"machine"} +Specified --scope as user + +Specified inputJSON as: +{"scope":"machine"} + +Specified inputJSON as: +{"scope":"machine"} +Specified --scope as user + +Specified inputJSON as: +{"ensure":"present"} +``` + +The DSC Resource is now fully implemented to handle input as arguments and as a JSON blob over +stdin. + +[05]: https://pkg.go.dev/github.com/spf13/pflag#Value diff --git a/samples/go/resources/first/docs/4-implement-get.md b/samples/go/resources/first/docs/4-implement-get.md new file mode 100644 index 0000000..f730351 --- /dev/null +++ b/samples/go/resources/first/docs/4-implement-get.md @@ -0,0 +1,309 @@ +--- +title: Step 4 - Implement get functionality +weight: 4 +dscs: + menu_title: 4. Implement get +--- + +To implement the get command, the DSC Resource needs to be able to find and marshal the settings +from a specific `tstoy` configuration file. + +Recall from [About the TSToy application][01] that you can use the `tstoy show path` command to get +the full path to the applications configuration files. The DSC Resource can use those commands +instead of trying to generate the paths itself. + +## Define get helper functions and methods { toc_md="Define `get` helpers" } + +Open the `config/config.go` file. In it, add the `getAppConfigPath` function. It should take a +`Scope` value as input and return a string and error. + +```go +func getAppConfigPath(s Scope) (string, error) { + args := []string{"show", "path", s.String()} + + output, err := exec.Command("tstoy", args...).Output() + if err != nil { + return "", err + } + + // We need to trim trailing whitespace automatically emitted for the path. + path := string(output) + path = strings.Trim(path, "\n") + path = strings.Trim(path, "\r") + + return path, nil +} +``` + +The function generates the arguments to send to `tstoy` and calls the command. + +Next, update the `Settings` struct to include a private field for the configuration path and +implement the public `GetConfigPath` function to retrieve the path for the instance of the +configuration file. + +```go +type Settings struct { + Ensure Ensure `json:"ensure,omitempty"` + Scope Scope `json:"scope,omitempty"` + UpdateAutomatically *bool `json:"updateAutomatically,omitempty"` + UpdateFrequency Frequency `json:"updateFrequency,omitempty"` + configPath string +} + +func (s *Settings) GetConfigPath() (string, error) { + if s.configPath == "" { + path, err := getAppConfigPath(s.Scope) + if err != nil { + return "", err + } + s.configPath = path + } + + return s.configPath, nil +} +``` + +The `GetConfigPath` function reduces the number of calls the DSC Resource needs to make to the +application when you implement the `set` command. + +Now that the DSC Resource can find the correct path, it needs to be able to retrieve settings from +the configuration file. You need to implement two more private functions: + +1. `getAppConfigMap` to retrieve the configuration file settings as a generic `map[string]any` + object. +1. `getAppConfigSettings` to convert the generic map into a `Settings` instance. + +First, implement `getAppConfigMap` to read the configuration file and unmarshal the JSON. + +```go +func getAppConfigMap(path string) (map[string]any, error) { + var config map[string]any + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + err = json.Unmarshal(data, &config) + return config, err +} +``` + +Next, implement `getAppConfigSettings` to convert the map into a `Settings` instance. + +```go +func getAppConfigSettings(scope Scope, config map[string]any) (Settings, error) { + // ensure the map keys are all strings. + maps.IntfaceKeysToStrings(config) + + // Since we found the config, we know the scope and ensure state. + settings := Settings{ + Scope: scope, + Ensure: EnsurePresent, + } + + // Check for the update settings + updates, ok := config["updates"] + if ok { + for key, value := range updates.(map[string]any) { + switch key { + case "automatic": + auto := value.(bool) + settings.UpdateAutomatically = &auto + case "checkFrequency": + intValue := int(value.(float64)) + frequency := Frequency(intValue) + settings.UpdateFrequency = frequency + } + } + } + + return settings, nil +} +``` + +With those private functions implemented, you can add methods to `Settings` for retrieving the map +of settings and the actual state. + +```go +func (s *Settings) GetConfigMap() (map[string]any, error) { + path, err := s.GetConfigPath() + if err != nil { + return nil, err + } + return getAppConfigMap(path) +} + +func (s *Settings) GetConfigSettings() (Settings, error) { + config, err := s.GetConfigMap() + if errors.Is(err, os.ErrNotExist) { + return Settings{ + Ensure: EnsureAbsent, + Scope: s.Scope, + }, nil + } else if err != nil { + return Settings{}, err + } + + return getAppConfigSettings(s.Scope, config) +} +``` + +## Update getState to return one instance { toc_md="Support returning one instance" } + +Open the `cmd/get.go` file and return to the `getState` function. Instead of printing the inputs, +the function should: + +1. Create an instance of `Settings` from the inputs. +1. Validate the instance. +1. Get the current settings from the system. +1. Print the results. + +```go +func getState(cmd *cobra.Command, args []string) error { + // Only the scope is used when retrieving current state. + s := config.Settings{ + Scope: targetScope, + } + + err := s.Validate() + if err != nil { + return fmt.Errorf("can't get settings; %s", err) + } + + config, err := s.GetConfigSettings() + if err != nil { + return fmt.Errorf("failed to get settings; %s", err) + } + + return config.Print() +} +``` + +Now you can run the updated command to see how it works: + +```sh +go run ./main.go get +go run ./main.go get --scope machine +go run ./main.go get --inputJSON '{ "scope": "user" }' +'{ "scope": "user" }' | go run ./main.go get --scope machine +go run ./main.go get --scope machine --ensure present +``` + +```Output +Error: can't get settings; the Scope setting isn't defined. Must define a Scope +for Settings + +{"ensure":"absent","scope":"machine"} + +{"ensure":"absent","scope":"user"} + +{"ensure":"absent","scope":"machine"} + +{"ensure":"absent","scope":"machine"} +``` + +## Update getState to return all instances { toc_md="Support returning all instances" } + +DSC Resources may optionally return the current state for every manageable instance. This is +convenient for users who want to get information about a resource with a single command. It's also +useful for higher-order tools that can cache current state. + +To add this functionality, add the `all` variable as a boolean in `cmd/get.go`. + +```go +var all bool +``` + +In the `init` function, add `--all` as a new flag for the command. + +```go +func init() { + rootCmd.AddCommand(getCmd) + getCmd.Flags().BoolVar( + &all, + "all", + false, + "Get the configurations for all scopes.", + ) +} +``` + +Update the `getState` function to handle the new flag by making the behavior loop. The function +should handle a few different cases for the input: + +- If the `--all` flag is used, the function should return the instance for both scopes. +- If the `--targetScope` flag is used, the function should return the instance for that scope. +- If `--targetScope` is used with a JSON blob from `--inputJSON` or stdin, the JSON value should be + ignored. +- If the command receives a JSON blob from `--inputJSON` or stdin without the `--targetScope` flag, + the command should use that value. + +```go +func getState(cmd *cobra.Command, args []string) error { + list := []config.Settings{} + if all { + list = append( + list, + config.Settings{Scope: config.ScopeMachine}, + config.Settings{Scope: config.ScopeUser}, + ) + } else if targetScope != config.ScopeUndefined { + // explicit --scope overrides JSON + list = append(list, config.Settings{Scope: targetScope}) + } else if inputJSON != nil { + list = append(list, *inputJSON) + } else { + // fails but with consistent messaging + list = append(list, config.Settings{Scope: targetScope}) + } + + for _, s := range list { + + err := s.Validate() + if err != nil { + return fmt.Errorf("can't get settings; %s", err) + } + + config, err := s.GetConfigSettings() + if err != nil { + return fmt.Errorf("failed to get settings; %s", err) + } + + err = config.Print() + if err != nil { + return err + } + } + + return nil +} +``` + +Run the updated command: + +```sh +go run ./main.go get --all +go run ./main.go get --scope machine +go run ./main.go get --inputJSON '{"scope": "user"}' +go run ./main.go get --inputJSON '{"scope": "user"}' --scope machine +'{ + "scope": "machine", + "ensure": "present" +}' | go run ./main.go get +``` + +```Output +{"ensure":"absent","scope":"machine"} +{"ensure":"absent","scope":"user"} + +{"ensure":"absent","scope":"machine"} + +{"ensure":"absent","scope":"user"} + +{"ensure":"absent","scope":"machine"} + +{"ensure":"absent","scope":"machine"} +``` + +[01]: /tstoy/about diff --git a/samples/go/resources/first/docs/5-implement-set.md b/samples/go/resources/first/docs/5-implement-set.md new file mode 100644 index 0000000..fdfa3c0 --- /dev/null +++ b/samples/go/resources/first/docs/5-implement-set.md @@ -0,0 +1,501 @@ +--- +title: Step 5 - Implement set functionality +weight: 5 +dscs: + menu_title: 5. Implement set +--- + +Up to this point, the DSC Resource has been primarily concerned with representing and getting the +current state of an instance. To be fully useful, it needs to be able to change a configuration file +to enforce the desired state. + +## Minimally implement set + +Open the `cmd/set.go` file. + +At the bottom of the file, create a new `setState` function that takes two parameters, a pointer to +`cobra.Command` and a slice of strings, and returns an error. + +```go +func setState(cmd *cobra.Command, args []string) error { + return nil +} +``` + +Replace the `Run` entry in the `setCmd` variable's definition with the `RunE` field set to the +`setState` function. Update the documentation for the command to be more specific to the DSC +Resource. + +```go +var setCmd = &cobra.Command{ + Use: "set", + Short: "Sets a tstoy configuration file to the desired state.", + Long: `The set command ensures that the tstoy configuration file for a +specific scope has the desired settings. It returns the updated settings state +as a JSON blob to stdout.`, + RunE: setState, +} +``` + +Next, update the `setState` function to convert the inputs into an instance of `Settings`. For now, +the function should validate the desired state and print it. + +```go +func setState(cmd *cobra.Command, args []string) error { + enforcing := config.Settings{} + + if inputJSON != nil { + enforcing = *inputJSON + } + if targetScope != config.ScopeUndefined { + enforcing.Scope = targetScope + } + if targetEnsure != config.EnsureUndefined { + enforcing.Ensure = targetEnsure + } + if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed { + enforcing.UpdateAutomatically = &updateAutomatically + } + if updateFrequency != 0 { + enforcing.UpdateFrequency = updateFrequency + } + + err := enforcing.Validate() + if err != nil { + return fmt.Errorf("can't enforce settings; %s", err) + } + + return enforcing.Print() +} +``` + +Verify the behavior for the set command. + +```sh +go run ./main.go set --scope machine --ensure present --updateAutomatically=false + +'{ + "scope": "user", + "ensure": "present", + "updateAutomatically": true, + "updateFrequency": 45 +}' | go run ./main.go set + +'{ + "scope": "user", + "ensure": "present", + "updateAutomatically": true, + "updateFrequency": 45 +}' | go run ./main.go set --ensure absent +``` + +```Output +{"ensure":"present","scope":"machine","updateAutomatically":false} + +{"ensure":"present","scope":"user","updateAutomatically":true,"updateFrequency":45} + +{"ensure":"absent","scope":"user","updateAutomatically":true,"updateFrequency":45} +``` + +## Implement helper functions and methods for set { toc_md="Implement `set` helpers" } + +At this point, the DSC Resource is able to validate the desired state. It needs to be able to +actually change the configuration files. + +Open `config/config.go` and define an `Enforce` method for `Settings` that returns a pointer to an +instance of `Settings` and an error. It should: + +1. Validate the settings. +1. Get the current settings for that scope. +1. Decide what it needs to do to enforce the desired state, if anything. + +```go +func (s *Settings) Enforce() (*Settings, error) { + err := s.Validate() + if err != nil { + return nil, err + } + + current, err := s.GetConfigSettings() + if err != nil { + return nil, err + } + + if s.Ensure == EnsureAbsent { + // remove the config file + } + + if current.Ensure == EnsureAbsent { + // create the config file + } + + // update the config file + return s, nil +} +``` + +This shows that the method needs to handle three different change types for the configuration file: + +1. It needs to remove the configuration file it's not supposed to exist. +1. It needs to create the configuration file when it's supposed to exist and doesn't exist. +1. It needs to update the configuration file when it's supposed to exist and does exist. + +Remember that a DSC Resource should be idempotent, only making changes when required. + +### Handle removing an instance + +Implement the remove method first. It should take an instance of `Settings` as input and return +both a pointer to an instance of `Settings` and an error. + +```go +func (s *Settings) remove(current Settings) (*Settings, error) { + if current.Ensure == EnsureAbsent { + return s, nil + } + + // At this point, s.GetConfigPath() has already run without an error, + // so we can rely on accessing the private field directly. + err := os.Remove(s.configPath) + if err != nil { + return ¤t, err + } + + return s, nil +} +``` + +If the file doesn't exist, the method returns the desired state and nil. If it does exist, the +method tries to delete the file. If the operation fails, it returns the current state and the error +message. If the operation succeeds, it returns the desired state and nil. + +### Handle creating an instance + +Next, implement the `create` method. It needs the same inputs and outputs as `remove`. It should +create the file and parent folders if needed, then compose the JSON for the configuration file and +write it. + +```go +func (s *Settings) create(currentSettings Settings) (*Settings, error) { + configDir := filepath.Dir(s.configPath) + if err := os.MkdirAll(configDir, 0750); err != nil { + return ¤tSettings, fmt.Errorf( + "failed to create folder for config file in '%s': %s", + configDir, + err, + ) + } + configFile, err := os.Create(s.configPath) + if err != nil { + return ¤tSettings, fmt.Errorf( + "failed to create config file '%s': %s", + s.configPath, + err, + ) + } + + // Create the JSON for the tstoy configuration file. + // Can't just marshal the Settings instance because it's a representation + // of the settings, not a literal blob of the settings. + settings := make(map[string]any) + updates := make(map[string]any) + addUpdates := false + if s.UpdateAutomatically != nil { + addUpdates = true + updates["automatic"] = *s.UpdateAutomatically + } + if s.UpdateFrequency != 0 { + addUpdates = true + updates["checkFrequency"] = s.UpdateFrequency + } + if addUpdates { + settings["updates"] = updates + } + + configJSON, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return ¤tSettings, fmt.Errorf( + "unable to convert settings to json: %s", + err, + ) + } + + _, err = configFile.Write(configJSON) + if err != nil { + return ¤tSettings, fmt.Errorf( + "unable to write config file: %s", + err, + ) + } + + return s, nil +} +``` + +### Handle updating an instance + +With `create` and `remove` implemented, the last method to implement is `update`. It needs the same +inputs and outputs as the others. It should: + +1. Retrieve the actual map of settings in the configuration file. +1. Update only the settings that are out of sync. +1. Only update the configuration file if at least one setting needs enforcing. + +```go +func (s *Settings) update(current Settings) (*Settings, error) { + writeConfig := false + + currentMap, err := current.GetConfigMap() + if err != nil { + return nil, err + } + + // ensure the map keys are all strings. + maps.IntfaceKeysToStrings(currentMap) + + // Check for the update settings + updates, ok := currentMap["updates"] + if !ok { + currentMap["updates"] = make(map[string]any) + updates = currentMap["updates"] + } + + // Only update if desired state defines UpdateAutomatically and: + // 1. Current state doesn't define it, or + // 2. Current state's setting doesn't match desired state. + shouldSetUA := false + if s.UpdateAutomatically != nil { + if current.UpdateAutomatically == nil { + shouldSetUA = true + } else if *s.UpdateAutomatically != *current.UpdateAutomatically { + shouldSetUA = true + } + } + + if shouldSetUA { + writeConfig = true + updates.(map[string]any)["automatic"] = *s.UpdateAutomatically + } else if current.UpdateAutomatically != nil { + updates.(map[string]any)["automatic"] = *current.UpdateAutomatically + } + + // Only update if desired state defines UpdateFrequency and: + // 1. Current state doesn't define it, or + // 2. Current state's setting doesn't match desired state. + if s.UpdateFrequency != 0 && s.UpdateFrequency != current.UpdateFrequency { + writeConfig = true + updates.(map[string]any)["checkFrequency"] = s.UpdateFrequency + } else if current.UpdateFrequency != 0 { + updates.(map[string]any)["checkFrequency"] = current.UpdateFrequency + } + + // no changes made, leave config untouched + if !writeConfig { + return s, nil + } + + currentMap["updates"] = updates.(map[string]any) + + configJson, err := json.MarshalIndent(currentMap, "", " ") + if err != nil { + return ¤t, fmt.Errorf( + "unable to convert updated settings to json: %s", + err, + ) + } + + err = os.WriteFile(s.configPath, configJson, 0750) + if err != nil { + return ¤t, fmt.Errorf( + "unable to write updated config file: %s", + err, + ) + } + + return s, nil +} +``` + +## Finish implementing `set` + +With the `create`, `remove`, and `update` methods implemented, update the `Enforce` method to +call them as required. + +```go +func (s *Settings) Enforce() (*Settings, error) { + err := s.Validate() + if err != nil { + return nil, err + } + + current, err := s.GetConfigSettings() + if err != nil { + return nil, err + } + + if s.Ensure == EnsureAbsent { + return s.remove(current) + } + + if current.Ensure == EnsureAbsent { + return s.create(current) + } + + return s.update(current) +} +``` + +Open `cmd/set.go` and edit the `setState` function to call the `Enforce` method. + +```go +func setState(cmd *cobra.Command, args []string) error { + enforcing := config.Settings{} + + if inputJSON != nil { + enforcing = *inputJSON + } + if targetScope != config.ScopeUndefined { + enforcing.Scope = targetScope + } + if targetEnsure != config.EnsureUndefined { + enforcing.Ensure = targetEnsure + } + if rootCmd.PersistentFlags().Lookup("updateAutomatically").Changed { + enforcing.UpdateAutomatically = &updateAutomatically + } + if updateFrequency != 0 { + enforcing.UpdateFrequency = updateFrequency + } + + final, err := enforcing.Enforce() + if err != nil { + return fmt.Errorf("can't enforce settings; %s", err) + } + + return final.Print() +} +``` + +## Verify behavior + +With the set command fully implemented, you can verify the behavior: + +1. Show TSToy's configuration information before changing any state. + + ```sh + tstoy show + ``` + + ```Output + Default configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } + } + Machine configuration: {} + User configuration: {} + Final configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } + } + ``` + +1. Run the `get` command to see how the DSC Resource reports on current state: + + ```sh + go run ./main.go get --scope machine + ``` + + ```json + {"ensure":"absent","scope":"machine"} + ``` + +1. Enforce the desired state with the `set` command. + + ```sh + go run ./main.go set --scope machine --ensure present --updateAutomatically=false + ``` + + ```json + {"ensure":"present","scope":"machine","updateAutomatically":false} + ``` + +1. Verify that the output from the `set` command matches the output from `get` after enforcing the + desired state. + + ```sh + go run ./main.go get --scope machine + ``` + + ```json + {"ensure":"present","scope":"machine","updateAutomatically":false} + ``` + +1. Use the `tstoy show` command to see how the configuration changes affected TSToy. + + ```sh + tstoy show --only machine,final + ``` + + ```Output + Machine configuration: { + "Updates": { + "Automatic": false + } + } + Final configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } + } + ``` + +1. Enforce desired state for the user-scope configuration file. + + ```sh + '{ + "scope": "user", + "ensure": "present", + "updateAutomatically": true, + "updateFrequency": 45 + }' | go run ./main.go set + ``` + + ```json + {"ensure":"present","scope":"user","updateAutomatically":true,"updateFrequency":45} + ``` + +1. Use the `tstoy show` command to see how the configuration changes affected TSToy. + + ```sh + tstoy show + ``` + + ```Output + Default configuration: { + "Updates": { + "Automatic": false, + "CheckFrequency": 90 + } + } + Machine configuration: { + "Updates": { + "Automatic": false + } + } + User configuration: { + "Updates": { + "Automatic": true, + "CheckFrequency": 45 + } + } + Final configuration: { + "Updates": { + "Automatic": true, + "CheckFrequency": 45 + } + } + ``` diff --git a/samples/go/resources/first/docs/6-author-manifest.md b/samples/go/resources/first/docs/6-author-manifest.md new file mode 100644 index 0000000..b5efd5d --- /dev/null +++ b/samples/go/resources/first/docs/6-author-manifest.md @@ -0,0 +1,165 @@ +--- +title: Step 6 - Author the DSC Resource manifest +weight: 6 +dscs: + menu_title: 6. Author manifest +--- + +The DSC Resource is now fully implemented. The last required step to use it with DSC is to author a +resource manifest. Command-based DSC Resources must have a JSON file that follows the naming +convention `.dsc.resource.json`. That's the manifest file for the resource. It +informs DSC and other higher-order tools about how the DSC Resource is implemented. + +Create a new file called `gotstoy.dsc.resource.json` in the project folder and open it. + +```sh +touch ./gotstoy.dsc.resource.json +code ./gotstoy.dsc.resource.json +``` + +Add basic metadata for the DSC Resource. + +```json +{ + "manifestVersion": "1.0", + "type": "TSToy.Example/gotstoy", + "version": "0.1.0", + "description": "A DSC Resource written in go to manage TSToy." +} +``` + +To inform DSC about how to get the current state of an instance, add the `get` key to the manifest. + +```json +{ + "manifestVersion": "1.0", + "type": "TSToy.Example/gotstoy", + "version": "0.1.0", + "description": "A DSC Resource written in go to manage TSToy.", + "get": { + "executable": "gotstoy", + "args": ["get"], + "input": "stdin" + } +} +``` + +The `executable` key indicates the name of the binary `dsc` should use. The `args` key indicates +that `dsc` should call `gotstoy get` to get the current state. The `input` key indicates that `dsc` +should pass the settings to the DSC Resource as a JSON blob over `stdin`. Even though the DSC +Resource can use argument flags, setting this value to JSON makes the integration more robust and +maintainable. + +Next, define the `set` key in the manifest to inform DSC how to enforce the desired state of an +instance. + +```json +{ + "manifestVersion": "1.0", + "type": "TSToy.Example/gotstoy", + "version": "0.1.0", + "description": "A DSC Resource written in go to manage TSToy.", + "get": { + "executable": "gotstoy", + "args": ["get"], + "input": "stdin" + }, + "set": { + "executable": "gotstoy", + "args": ["set"], + "input": "stdin", + "preTest": true, + "return": "state" + } +} +``` + +In this section of the manifest, the `preTest` option indicates that the DSC Resource validates the +instance state itself inside the set command. DSC won't test instances of the resource before +invoking the set operation. + +This section also defines the `return` key as `state`, which indicates that the resource returns +the current state of the instance when the command finishes. + +The last section of the manifest that needs to be defined is the `schema`. + +## Define the resource schema + +For this resource, add the JSON Schema representing valid settings in the `embedded` key. An +instance of the resource must meet these criteria: + +1. The instance must be an object. +1. The instance must define the `scope` property. +1. The `scope` property must be a string and set to either `machine` or `user`. +1. If the `ensure` property is specified, must be a string and set to either `present` or `absent`. + If `ensure` isn't specified, it should default to `present`. +1. If the `updateAutomatically` property is specified, it must be a boolean value. +1. If the `updateFrequency` property is specified, it must be an integer between `1` and `90`, + inclusive. + +```json +{ + "manifestVersion": "1.0", + "type": "TSToy.Example/gotstoy", + "version": "0.1.0", + "description": "A DSC Resource written in go to manage TSToy.", + "get": { + "executable": "gotstoy", + "args": ["get"], + "input": "stdin" + }, + "set": { + "executable": "gotstoy", + "args": ["set"], + "input": "stdin", + "preTest": true, + "return": "state" + }, + "schema": { + "embedded": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Golang TSToy Resource", + "type": "object", + "required": [ + "scope" + ], + "properties": { + "scope": { + "title": "Target configuration scope", + "description": "Defines which of TSToy's config files to manage.", + "type": "string", + "enum": [ + "machine", + "user" + ] + }, + "ensure": { + "title": "Ensure configuration file existence", + "description": "Defines whether the config file should exist.", + "type": "string", + "enum": [ + "present", + "absent" + ], + "default": "present" + }, + "updateAutomatically": { + "title": "Should update automatically", + "description": "Indicates whether TSToy should check for updates when it starts.", + "type": "boolean" + }, + "updateFrequency": { + "title": "Update check frequency", + "description": "Indicates how many days TSToy should wait before checking for updates.", + "type": "integer", + "minimum": 1, + "maximum": 90 + } + } + } + } +} +``` + +When authoring a JSON Schema, always include the `title` and `description` keys for every property. +Authoring tools, like VS Code, use those keys to give users context. diff --git a/samples/go/resources/first/docs/7-validate.md b/samples/go/resources/first/docs/7-validate.md new file mode 100644 index 0000000..5261676 --- /dev/null +++ b/samples/go/resources/first/docs/7-validate.md @@ -0,0 +1,301 @@ +--- +title: Step 7 - Validate the DSC Resource with DSC +weight: 7 +dscs: + menu_title: 7. Validate the resource +--- + +The DSC Resource is now fully implemented. + +## Build the resource + +To use it with DSC, you need to compile it and ensure DSC can find it in the `PATH`. + +``````tabs +````tab { name="Build on Windows" } +```powershell +go build -o gotstoy.exe . +$env:Path = $PWD.Path + ';' + $env:Path +``` +```` + +````tab { name="Build on Linux or macOS" } +```sh +go build -o gotstoy . +export PATH=$(pwd):$PATH +``` +```` +`````` + +## List the resource with DSC { toc_text="List the resource" } + +With the resource built and added to the PATH with its manifest, you can use it with DSC instead of +calling it directly. + +First, verify that DSC recognizes the DSC Resource. + +```sh +dsc resource list TSToy.Example/gotstoy +``` + +```yaml +type: TSToy.Example/gotstoy +version: '' +path: C:\code\dsc\gotstoy\gotstoy.dsc.resource.json +directory: C:\code\dsc\gotstoy +implementedAs: Command +author: null +properties: [] +requires: null +manifest: + manifestVersion: '1.0' + type: TSToy.Example/gotstoy + version: 0.1.0 + description: A DSC Resource written in go to manage TSToy. + get: + executable: gotstoy + args: + - get + input: stdin + set: + executable: gotstoy + args: + - set + input: stdin + preTest: true + return: state + schema: + embedded: + $schema: https://json-schema.org/draft/2020-12/schema + title: Golang TSToy Resource + type: object + required: + - scope + properties: + scope: + title: Target configuration scope + description: Defines which of TSToy's config files to manage. + type: string + enum: + - machine + - user + ensure: + title: Ensure configuration file existence + description: Defines whether the config file should exist. + type: string + enum: + - present + - absent + default: present + updateAutomatically: + title: Should update automatically + description: Indicates whether TSToy should check for updates when it starts. + type: boolean + updateFrequency: + title: Update check frequency + description: Indicates how many days TSToy should wait before checking for updates. + type: integer + minimum: 1 + maximum: 90 +``` + +## Manage state with `dsc resource` + +Get the current state of the machine-scope configuration file. + +```sh +'{ "scope": "machine" }' | dsc resource get --resource TSToy.Example/gotstoy +``` + +```yaml +actualState: + ensure: present + scope: machine + updateAutomatically: false +``` + +Test whether the user-scope configuration file is absent. + +```sh +'{ + "scope": "machine", + "ensure": "absent" +}' | dsc resource test --resource TSToy.Example/gotstoy +``` + +```yaml +expected_state: + scope: machine + ensure: absent +actualState: + ensure: present + scope: machine + updateAutomatically: false +differingProperties: +- ensure +``` + +Remove the machine-scope configuration file. + +```sh +'{ + "scope": "machine", + "ensure": "absent" +}' | dsc resource set --resource TSToy.Example/gotstoy +``` + +```yaml +beforeState: + ensure: present + scope: machine + updateAutomatically: false +afterState: + ensure: absent + scope: machine +changedProperties: +- ensure +``` + +## Manage state with `dsc config` + +Save the following configuration file as `gotstoy.dsc.config.yaml`. It defines an instance for both +configuration scopes, disabling automatic updates in the machine scope and enabling it with a +30-day frequency in the user scope. + +```yaml +$schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json +resources: +- name: All Users Configuration + type: TSToy.Example/gotstoy + properties: + scope: machine + ensure: present + updateAutomatically: false +- name: Current User Configuration + type: TSToy.Example/gotstoy + properties: + scope: user + ensure: present + updateAutomatically: true + updateFrequency: 30 +``` + +Get the current state of the instances defined in the configuration: + +```sh +cat gotstoy.dsc.config.yaml | dsc config get +``` + +```yaml +results: +- name: All Users Configuration + type: TSToy.Example/gotstoy + result: + actualState: + ensure: absent + scope: machine +- name: Current User Configuration + type: TSToy.Example/gotstoy + result: + actualState: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 +messages: [] +hadErrors: false +``` + +The command returns a result for each instance in the configuration, showing the instance's name, +resource type, and actual state. The resource didn't raise any errors or emit any messages. + +Next, test whether the instances are in the desired state: + +```sh +cat gotstoy.dsc.config.yaml | dsc config test +``` + +```yaml +results: +- name: All Users Configuration + type: TSToy.Example/gotstoy + result: + desiredState: + updateAutomatically: false + scope: machine + ensure: present + actualState: + ensure: absent + scope: machine + differingProperties: + - updateAutomatically + - ensure +- name: Current User Configuration + type: TSToy.Example/gotstoy + result: + desiredState: + ensure: present + scope: user + updateFrequency: 30 + updateAutomatically: true + actualState: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 + differingProperties: + - updateFrequency +messages: [] +hadErrors: false +``` + +The results show that both resources are out of the desired state. The machine scope configuration +file doesn't exist, while the user scope configuration file has an incorrect value for the update +frequency. + +Enforce the configuration with the `set` command. + +```sh +cat gotstoy.dsc.config.yaml | dsc config set +``` + +```yaml +results: +- name: All Users Configuration + type: TSToy.Example/gotstoy + result: + beforeState: + ensure: absent + scope: machine + afterState: + ensure: present + scope: machine + updateAutomatically: false + changedProperties: + - ensure + - updateAutomatically +- name: Current User Configuration + type: TSToy.Example/gotstoy + result: + beforeState: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 45 + afterState: + ensure: present + scope: user + updateAutomatically: true + updateFrequency: 30 + changedProperties: + - updateFrequency +messages: [] +hadErrors: false +``` + +The results show that the resource created the machine scope configuration file and set the +`updateAutomatically` property for it. The results also show that the resource changed the update +frequency for the user scope configuration file. + +Together, these steps minimally confirm that the resource can be used with DSC. DSC is able to get, +test, and set resource instances individually and in configuration documents. diff --git a/samples/go/resources/first/docs/_index.md b/samples/go/resources/first/docs/_index.md new file mode 100644 index 0000000..ea0ac0e --- /dev/null +++ b/samples/go/resources/first/docs/_index.md @@ -0,0 +1,53 @@ +--- +title: Write your first DSC Resource in Go +dscs: + tutorials_title: In Go + languages_title: Write a DSC Resource +platen: + menu: + collapse_section: true +--- + +With DSC v3, you can author command-based DSC Resources in any language. This enables you to manage +applications in the programming language you and your team prefer, or in the same language as the +application you're managing. + +This tutorial describes how you can implement a DSC Resource in Go to manage an application's +configuration files. While this tutorial creates a resource to manage the fictional +[TSToy application][02], the principles apply when you author any command-based resource. + +In this tutorial, you learn how to: + +- Create a small Go application to use as a DSC Resource. +- Define the properties of the resource. +- Implement `get` and `set` commands for the resource. +- Write a manifest for the resource. +- Manually test the resource. + +## Prerequisites + +- Familiarize yourself with the structure of a command-based DSC Resource. +- Read [About the TSToy application][02], install `tstoy`, and add it to your `PATH`. +- Go 1.19 or higher +- VS Code with the Go extension + +## Steps + +1. [Create the DSC Resource][03] +1. [Define the configuration settings][04] +1. [Handle input][05] +1. [Implement get][06] +1. [Implement set][07] +1. [Author the DSC Resource manifest][08] +1. [Validate the DSC Resource with DSC][09] +1. [Review and next steps][10] + +[02]: /tstoy/about/ +[03]: 1-create.md +[04]: 2-define-config-settings.md +[05]: 3-handle-input.md +[06]: 4-implement-get.md +[07]: 5-implement-set.md +[08]: 6-author-manifest.md +[09]: 7-validate.md +[10]: review.md diff --git a/samples/go/resources/first/docs/review.md b/samples/go/resources/first/docs/review.md new file mode 100644 index 0000000..1b9b4eb --- /dev/null +++ b/samples/go/resources/first/docs/review.md @@ -0,0 +1,36 @@ +--- +title: Review and next steps +weight: 100 +--- + +In this tutorial, you: + +1. Scaffolded a new Go app as a DSC Resource. +1. Defined the configurable settings to manage the TSToy application's configuration files and + update behavior. +1. Added flags to enable users to configure TSToy in the terminal with validation and completion + suggestions. +1. Added handling so the DSC Resource can use JSON input with a flag or from `stdin`. +1. Implemented the `get` command to return the current state of a TSToy configuration file as an + instance of the DSC Resource. +1. Added handling so the `get` command can retrieve every instance of the DSC Resource. +1. Implemented the `set` command to idempotently enforce the desired state for TSToy's + configuration files. +1. Tested the DSC Resource as a standalone application. +1. Authored a DSC Resource manifest and defined a JSON Schema for instances of the DSC Resource. +1. Tested the integration of the DSC Resource with DSC itself. + +At the end of this implementation, you have a functional command-based DSC Resource written in Go. + +## Clean up + +If you're not going to continue to work with this DSC Resource, delete the `gotstoy` folder and the +files in it. + +## Next steps + +1. Read about command-based DSC Resources, learn how they work, and consider why the DSC Resource + in this tutorial is implemented this way. +1. Consider how this DSC Resource can be improved. Are there any edge cases or features it doesn't + handle? Can you make the user experience in the terminal more delightful? Update the + implementation with your improvements. diff --git a/samples/go/resources/first/go.mod b/samples/go/resources/first/go.mod new file mode 100644 index 0000000..2c9976e --- /dev/null +++ b/samples/go/resources/first/go.mod @@ -0,0 +1,18 @@ +module github.com/PowerShell/DSC-Samples/go/resources/first + +go 1.19 + +require ( + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/thediveo/enumflag v0.10.1 // indirect + golang.org/x/sys v0.6.0 // indirect +) diff --git a/samples/go/resources/first/go.sum b/samples/go/resources/first/go.sum new file mode 100644 index 0000000..9b7656d --- /dev/null +++ b/samples/go/resources/first/go.sum @@ -0,0 +1,165 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/thediveo/enumflag v0.10.1 h1:DB3Ag69VZ7BCv6jzKECrZ0ebZrHLzFRMIFYt96s4OxM= +github.com/thediveo/enumflag v0.10.1/go.mod h1:KyVhQUPzreSw85oJi2uSjFM0ODLKXBH0rPod7zc2pmI= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/samples/go/resources/first/gotstoy.dsc.config.yaml b/samples/go/resources/first/gotstoy.dsc.config.yaml new file mode 100644 index 0000000..cbbff5f --- /dev/null +++ b/samples/go/resources/first/gotstoy.dsc.config.yaml @@ -0,0 +1,15 @@ +$schema: https://schemas.microsoft.com/dsc/2023/03/configuration.schema.json +resources: +- name: All Users Configuration + type: TSToy.Example/gotstoy + properties: + scope: machine + ensure: present + updateAutomatically: false +- name: Current User Configuration + type: TSToy.Example/gotstoy + properties: + scope: user + ensure: present + updateAutomatically: true + updateFrequency: 30 diff --git a/samples/go/resources/first/gotstoy.dsc.resource.json b/samples/go/resources/first/gotstoy.dsc.resource.json new file mode 100644 index 0000000..0a1ee63 --- /dev/null +++ b/samples/go/resources/first/gotstoy.dsc.resource.json @@ -0,0 +1,61 @@ +{ + "manifestVersion": "1.0", + "type": "TSToy.Example/gotstoy", + "version": "0.1.0", + "description": "A DSC Resource written in go to manage TSToy.", + "get": { + "executable": "gotstoy", + "args": ["get"], + "input": "stdin" + }, + "set": { + "executable": "gotstoy", + "args": ["set"], + "input": "stdin", + "preTest": true, + "return": "state" + }, + "schema": { + "embedded": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Golang TSToy Resource", + "type": "object", + "required": [ + "scope" + ], + "properties": { + "scope": { + "title": "Target configuration scope", + "description": "Defines which of TSToy's config files to manage.", + "type": "string", + "enum": [ + "machine", + "user" + ] + }, + "ensure": { + "title": "Ensure configuration file existence", + "description": "Defines whether the config file should exist.", + "type": "string", + "enum": [ + "present", + "absent" + ], + "default": "present" + }, + "updateAutomatically": { + "title": "Should update automatically", + "description": "Indicates whether TSToy should check for updates when it starts.", + "type": "boolean" + }, + "updateFrequency": { + "title": "Update check frequency", + "description": "Indicates how many days TSToy should wait before checking for updates.", + "type": "integer", + "minimum": 1, + "maximum": 90 + } + } + } + } +} \ No newline at end of file diff --git a/samples/go/resources/first/input/input.go b/samples/go/resources/first/input/input.go new file mode 100644 index 0000000..03acd61 --- /dev/null +++ b/samples/go/resources/first/input/input.go @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package input + +import ( + "encoding/json" + "io" + "os" + "strings" +) + +type JSONFlag struct { + Target any +} + +func (f *JSONFlag) String() string { + b, err := json.Marshal(f.Target) + if err != nil { + return "failed to marshal object" + } + return string(b) +} + +func (f *JSONFlag) Set(v string) error { + return json.Unmarshal([]byte(v), f.Target) +} + +func (f *JSONFlag) Type() string { + return "json" +} + +func HandleStdIn(args []string) []string { + info, _ := os.Stdin.Stat() + if (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice { + // do nothing + } else { + stdin, err := io.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + // remove surrounding whitespace + jsonBlob := strings.Trim(string(stdin), "\n") + jsonBlob = strings.Trim(jsonBlob, "\r") + jsonBlob = strings.TrimSpace(jsonBlob) + // only add to arguments if the string is non-empty. + if jsonBlob != "" { + args = append(args, "--inputJSON", jsonBlob) + } + } + + return args +} diff --git a/samples/go/resources/first/main.go b/samples/go/resources/first/main.go new file mode 100644 index 0000000..98b4533 --- /dev/null +++ b/samples/go/resources/first/main.go @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package main + +import ( + "os" + + "github.com/PowerShell/DSC-Samples/go/resources/first/cmd" + "github.com/PowerShell/DSC-Samples/go/resources/first/input" +) + +func main() { + args := []string{} + for index, arg := range os.Args { + // skip the first index, because it's the application name + if index > 0 { + args = append(args, arg) + } + } + + // Check stdin and add any found JSON blob after an --inputJSON flag. + args = input.HandleStdIn(args) + + // execute with the combined arguments + cmd.Execute(args) +}