Skip to content

Commit

Permalink
Add rad bicep publish-extension command (#8183)
Browse files Browse the repository at this point in the history
# Description

This new command will transform a resource provider manifest into an
extension that the Bicep compiler and editor can understand. The new
command shells out to `bicep publish-extension`.

Functional testing will be covered as part of the overall test plan for
the UDT feature.


## Type of change

- This pull request adds or changes features of Radius and has an
approved issue (issue link required).


Part of: #6688

## Contributor checklist
Please verify that the PR meets the following requirements, where
applicable:

- [ ] An overview of proposed schema changes is included in a linked
GitHub issue.
- [ ] A design document PR is created in the [design-notes
repository](https://github.com/radius-project/design-notes/), if new
APIs are being introduced.
- [ ] If applicable, design document has been reviewed and approved by
Radius maintainers/approvers.
- [ ] A PR for the [samples
repository](https://github.com/radius-project/samples) is created, if
existing samples are affected by the changes in this PR.
- [ ] A PR for the [documentation
repository](https://github.com/radius-project/docs) is created, if the
changes in this PR affect the documentation or any user facing updates
are made.
- [ ] A PR for the [recipes
repository](https://github.com/radius-project/recipes) is created, if
existing recipes are affected by the changes in this PR.

Signed-off-by: Ryan Nowak <[email protected]>
  • Loading branch information
rynowak authored Jan 5, 2025
1 parent 04f30c3 commit fa1078f
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 5 deletions.
4 changes: 4 additions & 0 deletions cmd/rad/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
app_show "github.com/radius-project/radius/pkg/cli/cmd/app/show"
app_status "github.com/radius-project/radius/pkg/cli/cmd/app/status"
bicep_publish "github.com/radius-project/radius/pkg/cli/cmd/bicep/publish"
bicep_publishextension "github.com/radius-project/radius/pkg/cli/cmd/bicep/publishextension"
credential "github.com/radius-project/radius/pkg/cli/cmd/credential"
cmd_deploy "github.com/radius-project/radius/pkg/cli/cmd/deploy"
env_create "github.com/radius-project/radius/pkg/cli/cmd/env/create"
Expand Down Expand Up @@ -344,6 +345,9 @@ func initSubCommands() {
bicepPublishCmd, _ := bicep_publish.NewCommand(framework)
bicepCmd.AddCommand(bicepPublishCmd)

bicepPublishExtensionCmd, _ := bicep_publishextension.NewCommand(framework)
bicepCmd.AddCommand(bicepPublishExtensionCmd)

installCmd := install.NewCommand()
RootCmd.AddCommand(installCmd)

Expand Down
10 changes: 7 additions & 3 deletions pkg/cli/bicep/bicep.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ const (
retryDelaySecs = 5
)

func GetBicepFilePath() (string, error) {
return tools.GetLocalFilepath(radBicepEnvVar, binaryName)
}

// IsBicepInstalled returns true if our local copy of bicep is installed
//

// IsBicepInstalled checks if the Bicep binary is installed on the local machine and returns a boolean and an error if one occurs.
func IsBicepInstalled() (bool, error) {
filepath, err := tools.GetLocalFilepath(radBicepEnvVar, binaryName)
filepath, err := GetBicepFilePath()
if err != nil {
return false, err
}
Expand All @@ -53,7 +57,7 @@ func IsBicepInstalled() (bool, error) {

// DeleteBicep cleans our local copy of bicep
func DeleteBicep() error {
filepath, err := tools.GetLocalFilepath(radBicepEnvVar, binaryName)
filepath, err := GetBicepFilePath()
if err != nil {
return err
}
Expand All @@ -72,7 +76,7 @@ func DeleteBicep() error {
// DownloadBicep() attempts to download a file from a given URI and save it to a local filepath, retrying up to 10 times if
// the download fails. If an error occurs, an error is returned.
func DownloadBicep() error {
filepath, err := tools.GetLocalFilepath(radBicepEnvVar, binaryName)
filepath, err := GetBicepFilePath()
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/cmd/bicep/publish/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package bicep
package publish

import (
"bytes"
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/cmd/bicep/publish/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package bicep
package publish

import (
"context"
Expand Down
182 changes: 182 additions & 0 deletions pkg/cli/cmd/bicep/publishextension/publish.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
Copyright 2023 The Radius Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package publishextension

import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"

"github.com/radius-project/radius/pkg/cli/bicep"
"github.com/radius-project/radius/pkg/cli/clierrors"
"github.com/radius-project/radius/pkg/cli/cmd/commonflags"
"github.com/radius-project/radius/pkg/cli/framework"
"github.com/radius-project/radius/pkg/cli/manifest"
"github.com/radius-project/radius/pkg/cli/output"

"github.com/spf13/cobra"
)

// NewCommand creates a new instance of the `rad bicep publish-extension` command.
func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) {
runner := NewRunner(factory)

cmd := &cobra.Command{
Use: "publish-extension",
Short: "Generate or publish a Bicep extension for a set of resource types.",
Long: `Generate or publish a Bicep extension for a set of resource types.
This command compiles a set of resource types (resource provider manifest) into a Bicep extension for local use or distribution.
Bicep extensions enable extensibility for the Bicep language. This command can be used to generate and distribute Bicep support for resource types authored by users. Bicep extensions can be distributed using Open Container Initiative (OCI) registry, such as Azure Container Registry, Docker Hub, or GitHub Container Registry. See https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-extension for more information on Bicep extensions.
Once an extension is been generated, it can be used locally or published to a container registry for distribution depending on the target specified.
When publishing to an OCI registry it is expected the user runs docker login (or similar command) and has the proper permission to push to the target OCI registry.
`,
Example: `
# Generate a Bicep extension to a local file
rad bicep publish-extension --from-file ./Example.Provider.yaml --target ./output.tgz
# Publish a Bicep extension to a container registry
bicep publish-extension ./Example.Provider.yaml --target br:ghcr.io/myregistry/example-provider:v1
`,
Args: cobra.ExactArgs(0),
RunE: framework.RunCommand(runner),
}

commonflags.AddFromFileFlagVar(cmd, &runner.ResourceProviderManifestFilePath)
_ = cmd.MarkFlagRequired("from-file")
_ = cmd.MarkFlagFilename("from-file", "yaml", "json")

cmd.Flags().StringVar(&runner.Target, "target", "", "The destination path file or OCI registry path. OCI registry paths use the format 'br:HOST/PATH:TAG'.")
_ = cmd.MarkFlagRequired("target")
return cmd, runner
}

// Runner is the runner implementation for the `rad bicep publish-extension` command.
type Runner struct {
Output output.Interface

ResourceProvider *manifest.ResourceProvider
ResourceProviderManifestFilePath string
Target string
}

// NewRunner creates a new instance of the `rad bicep publish-extension` runner.
func NewRunner(factory framework.Factory) *Runner {
return &Runner{
Output: factory.GetOutput(),
}
}

// Validate validates the `rad bicep publish-extension` command.
func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
// We read the resource provider manifest upfront to ensure it exists and is valid.
//
// The validation we implement in the `rad` CLI is the source of truth for the manifest. The
// manifest-to-bicep-extension tool does minimal validation, so we want to catch any issues
// early.
rp, err := manifest.ReadFile(r.ResourceProviderManifestFilePath)
if err != nil {
return clierrors.MessageWithCause(err, "Failed to read resource provider %q", r.ResourceProviderManifestFilePath)
}

r.ResourceProvider = rp

return nil
}

// Run runs the `rad bicep publish-extension` command.
func (r *Runner) Run(ctx context.Context) error {
// This command ties together two separate shell commands:
// 1. We use NPX to run https://github.com/radius-project/bicep-tools/tree/main/packages/manifest-to-bicep-extension
// - This generates a Bicep extension "index"
// 2. We use `bicep publish-extension` to publish the extension "index" to the "target"
//
// 3. We can clean up the "index" directory after publishing.

_, err := exec.LookPath("npx")
if errors.Is(err, exec.ErrNotFound) {
return clierrors.Message("The command 'npx' was not found on the PATH. Please install Node.js 16+ to use this command.")
}

temp, err := os.MkdirTemp("", "bicep-extension-*")
if err != nil {
return err
}

defer os.RemoveAll(temp)

err = generateBicepExtensionIndex(ctx, r.ResourceProviderManifestFilePath, temp)
if err != nil {
return err
}

err = publishExtension(ctx, temp, r.Target)
if err != nil {
return err
}

r.Output.LogInfo("Successfully published Bicep extension %q to %q", r.ResourceProviderManifestFilePath, r.Target)
return nil
}

func generateBicepExtensionIndex(ctx context.Context, inputFilePath string, outputDirectoryPath string) error {
// npx @radius-project/manifest-to-bicep-extension@alpha generate <resource provider> <temp>
args := []string{
"@radius-project/manifest-to-bicep-extension@alpha",
"generate",
inputFilePath,
outputDirectoryPath,
}
cmd := exec.CommandContext(ctx, "npx", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()
if err != nil {
return clierrors.MessageWithCause(err, "Failed to generate Bicep extension")
}

return nil
}

func publishExtension(ctx context.Context, inputDirectoryPath string, target string) error {
bicepFilePath, err := bicep.GetBicepFilePath()
if err != nil {
return err
}

// rad-bicep publish-extension <temp>/index.json --target <target>
args := []string{
"publish-extension",
filepath.Join(inputDirectoryPath, "index.json"),
"--target", target,
}
cmd := exec.CommandContext(ctx, bicepFilePath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
if err != nil {
return clierrors.MessageWithCause(err, "Failed to publish Bicep extension")
}

return nil
}
47 changes: 47 additions & 0 deletions pkg/cli/cmd/bicep/publishextension/publish_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2023 The Radius Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package publishextension

import (
"testing"

"github.com/radius-project/radius/test/radcli"
)

// NOTE: this command orchestrates other CLI commands, and so it's not very testable. This will be covered with
// functional tests.

func TestRunner_Validate(t *testing.T) {
tests := []radcli.ValidateInput{
{
Name: "Valid",
Input: []string{"--from-file", "testdata/valid.yaml", "--target", "./output.tgz"},
ExpectedValid: true,
},
{
Name: "Invalid: invalid manifest",
Input: []string{"--from-file", "testdata/invalid.yaml", "--target", "./output.tgz"},
ExpectedValid: false,
},
{
Name: "Invalid: missing required options",
Input: []string{"--from-file", "testdata/valid.yaml"},
ExpectedValid: false,
},
}
radcli.SharedValidateValidation(t, NewCommand, tests)
}
6 changes: 6 additions & 0 deletions pkg/cli/cmd/bicep/publishextension/testdata/invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: MyCompany.Resources
types:
testResources dkdkkdkfkkd:
apiVersions:
'2025-01-01-preview':
schema: {}
6 changes: 6 additions & 0 deletions pkg/cli/cmd/bicep/publishextension/testdata/valid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: MyCompany.Resources
types:
testResources:
apiVersions:
'2025-01-01-preview':
schema: {}

0 comments on commit fa1078f

Please sign in to comment.