Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): commands for creating asset and asset scan #500

Merged
10 commits merged into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions backend/pkg/database/gorm/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ func (t *AssetsTableHandler) DeleteAsset(assetID models.AssetID) error {
return nil
}

// nolint: cyclop
func (t *AssetsTableHandler) checkUniqueness(asset models.Asset) (*models.Asset, error) {
discriminator, err := asset.AssetInfo.ValueByDiscriminator()
if err != nil {
Expand All @@ -310,6 +311,24 @@ func (t *AssetsTableHandler) checkUniqueness(asset models.Asset) (*models.Asset,
}
}
return nil, nil // nolint:nilnil
case models.DirInfo:
var assets []Asset
// In the case of creating or updating a asset, needs to be checked whether other asset exists with same DirName and Location.
filter := fmt.Sprintf("id ne '%s' and assetInfo/dirName eq '%s' and assetInfo/location eq '%s'", *asset.Id, *info.DirName, *info.Location)
err = ODataQuery(t.DB, assetSchemaName, &filter, nil, nil, nil, nil, nil, true, &assets)
if err != nil {
return nil, err
}
if len(assets) > 0 {
var apiAsset models.Asset
if err := json.Unmarshal(assets[0].Data, &apiAsset); err != nil {
return nil, fmt.Errorf("failed to convert DB model to API model: %w", err)
}
return &apiAsset, &common.ConflictError{
Reason: fmt.Sprintf("Asset directory exists with same name=%q and location=%q", *info.DirName, *info.Location),
}
}
return nil, nil // nolint:nilnil
default:
return nil, fmt.Errorf("asset type is not supported (%T): %w", discriminator, err)
}
Expand Down
143 changes: 143 additions & 0 deletions cli/cmd/asset_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright © 2023 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// 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 cmd

import (
"context"
"errors"
"fmt"
"os"
"time"

"github.com/spf13/cobra"

"github.com/openclarity/vmclarity/api/models"
"github.com/openclarity/vmclarity/shared/pkg/backendclient"
)

// assetCreateCmd represents the standalone command.
var assetCreateCmd = &cobra.Command{
Use: "asset-create",
Short: "Create asset",
Long: `It creates asset. It's useful in the CI/CD mode without VMClarity orchestration`,
Run: func(cmd *cobra.Command, args []string) {
logger.Infof("Creating asset...")
filename, err := cmd.Flags().GetString("from-json-file")
This conversation was marked as resolved.
Show resolved Hide resolved
if err != nil {
logger.Fatalf("Unable to get asset json file name: %v", err)
}
server, err := cmd.Flags().GetString("server")
if err != nil {
logger.Fatalf("Unable to get VMClarity server address: %v", err)
}

assetType, err := getAssetFromJSONFile(filename)
if err != nil {
logger.Fatalf("Failed to get asset from json file: %v", err)
}

_, err = assetType.ValueByDiscriminator()
if err != nil {
logger.Fatalf("Failed to determine asset type: %v", err)
}

assetID, err := createAsset(context.TODO(), assetType, server)
if err != nil {
logger.Fatalf("Failed to create asset: %v", err)
}
fmt.Println(assetID)
This conversation was marked as resolved.
Show resolved Hide resolved
},
}

func init() {
rootCmd.AddCommand(assetCreateCmd)

assetCreateCmd.Flags().String("from-json-file", "", "asset json filename")
assetCreateCmd.Flags().String("server", "", "VMClarity server to create asset to, for example: http://localhost:9999/api")
if err := assetCreateCmd.MarkFlagRequired("from-json-file"); err != nil {
logger.Fatalf("Failed to mark from-json-file flag as required: %v", err)
}
if err := assetCreateCmd.MarkFlagRequired("server"); err != nil {
logger.Fatalf("Failed to mark server flag as required: %v", err)
}
}

func getAssetFromJSONFile(filename string) (*models.AssetType, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %v", err)
}
defer file.Close()

// get the file size
stat, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file stat: %v", err)
}
// read the file
bs := make([]byte, stat.Size())
_, err = file.Read(bs)
if err != nil {
return nil, fmt.Errorf("failed to read file: %v", err)
}

assetType := &models.AssetType{}
if err := assetType.UnmarshalJSON(bs); err != nil {
return nil, fmt.Errorf("failed to unmarshal asset into AssetType %v", err)
}

return assetType, nil
}

func createAsset(ctx context.Context, assetType *models.AssetType, server string) (string, error) {
client, err := backendclient.Create(server)
if err != nil {
return "", fmt.Errorf("failed to create VMClarity API client: %w", err)
}

creationTime := time.Now()
assetData := models.Asset{
AssetInfo: assetType,
LastSeen: &creationTime,
FirstSeen: &creationTime,
}
asset, err := client.PostAsset(ctx, assetData)
if err == nil {
return *asset.Id, nil
}
var conflictError backendclient.AssetConflictError
if !errors.As(err, &conflictError) {
// If there is an error, and it's not a conflict telling
// us that the asset already exists, then we need to
// keep track of it and log it as a failure to
// complete discovery. We don't fail instantly here
// because discovering the assets is a heavy operation,
// so we want to give the best chance to create all the
// assets in the DB before failing.
return "", fmt.Errorf("failed to post asset: %v", err)
This conversation was marked as resolved.
Show resolved Hide resolved
}

// As we got a conflict it means there is an existing asset
// which matches the unique properties of this asset, in this
// case we'll patch the just AssetInfo and FirstSeen instead.
assetData.FirstSeen = nil
err = client.PatchAsset(ctx, assetData, *conflictError.ConflictingAsset.Id)
if err != nil {
return "", fmt.Errorf("failed to patch asset: %v", err)
}

return *conflictError.ConflictingAsset.Id, nil
}
104 changes: 104 additions & 0 deletions cli/cmd/asset_scan_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright © 2023 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// 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 cmd

import (
"context"
"errors"
"fmt"

"github.com/spf13/cobra"

"github.com/openclarity/vmclarity/api/models"
"github.com/openclarity/vmclarity/shared/pkg/backendclient"
"github.com/openclarity/vmclarity/shared/pkg/utils"
)

// assetScanCreateCmd represents the standalone command.
var assetScanCreateCmd = &cobra.Command{
Use: "asset-scan-create",
Short: "Create asset scan",
Long: `It creates asset scan. It's useful in the CI/CD mode without VMClarity orchestration`,
Run: func(cmd *cobra.Command, args []string) {
logger.Infof("asset-scan-create called")
assetID, err := cmd.Flags().GetString("asset-id")
if err != nil {
logger.Fatalf("Unable to get asset id: %v", err)
}
server, err := cmd.Flags().GetString("server")
if err != nil {
logger.Fatalf("Unable to get VMClarity server address: %v", err)
}
assetScanID, err := createAssetScan(context.TODO(), server, assetID)
if err != nil {
logger.Fatalf("Failed to create asset scan: %v", err)
}
fmt.Println(assetScanID)
},
}

func init() {
rootCmd.AddCommand(assetScanCreateCmd)
assetScanCreateCmd.Flags().String("server", "", "VMClarity server to create asset to, for example: http://localhost:9999/api")
assetScanCreateCmd.Flags().String("asset-id", "", "Asset ID for asset scan")
if err := assetScanCreateCmd.MarkFlagRequired("server"); err != nil {
logger.Fatalf("Failed to mark server flag as required: %v", err)
}
if err := assetScanCreateCmd.MarkFlagRequired("asset-id"); err != nil {
logger.Fatalf("Failed to mark asset-id flag as required: %v", err)
}
}

func createAssetScan(ctx context.Context, server, assetID string) (string, error) {
client, err := backendclient.Create(server)
if err != nil {
return "", fmt.Errorf("failed to create VMClarity API client: %w", err)
}

asset, err := client.GetAsset(ctx, assetID, models.GetAssetsAssetIDParams{})
if err != nil {
return "", fmt.Errorf("failed to get asset %s: %w", assetID, err)
}
assetScanData := createEmptyAssetScanForAsset(asset)

assetScan, err := client.PostAssetScan(ctx, assetScanData)
if err != nil {
var conErr backendclient.AssetScanConflictError
if errors.As(err, &conErr) {
assetScanID := *conErr.ConflictingAssetScan.Id
logger.WithField("AssetScanID", assetScanID).Debug("AssetScan already exist.")
return *conErr.ConflictingAssetScan.Id, nil
}
return "", fmt.Errorf("failed to post AssetScan to backend API: %w", err)
}

return *assetScan.Id, nil
}

func createEmptyAssetScanForAsset(asset models.Asset) models.AssetScan {
return models.AssetScan{
Asset: &models.AssetRelationship{
AssetInfo: asset.AssetInfo,
FirstSeen: asset.FirstSeen,
This conversation was marked as resolved.
Show resolved Hide resolved
Id: *asset.Id,
},
This conversation was marked as resolved.
Show resolved Hide resolved
Status: &models.AssetScanStatus{
General: &models.AssetScanState{
State: utils.PointerTo(models.AssetScanStateStateReadyToScan),
This conversation was marked as resolved.
Show resolved Hide resolved
},
},
}
}
Loading
Loading