diff --git a/docs/command/atlas-streams-instances-download.txt b/docs/command/atlas-streams-instances-download.txt new file mode 100644 index 0000000000..b73e63eeac --- /dev/null +++ b/docs/command/atlas-streams-instances-download.txt @@ -0,0 +1,113 @@ +.. _atlas-streams-instances-download: + +================================ +atlas streams instances download +================================ + +.. default-domain:: mongodb + +.. contents:: On this page + :local: + :backlinks: none + :depth: 1 + :class: singlecol + +Download a compressed file that contains the logs for the specified Atlas Stream Processing instance. + +This command downloads a file with a .gz extension. To use this command, you must authenticate with a user account or an API key with the Project Data Access Read/Write role. + +Syntax +------ + +.. code-block:: + :caption: Command Syntax + + atlas streams instances download [options] + +.. Code end marker, please don't delete this comment + +Arguments +--------- + +.. list-table:: + :header-rows: 1 + :widths: 20 10 10 60 + + * - Name + - Type + - Required + - Description + * - tenantName + - string + - true + - Label that identifies the tenant that stores the log files that you want to download. + +Options +------- + +.. list-table:: + :header-rows: 1 + :widths: 20 10 10 60 + + * - Name + - Type + - Required + - Description + * - --end + - int + - false + - UNIX Epoch-formatted ending date and time for the range of log messages to retrieve. This value defaults to the current timestamp. + * - --force + - + - false + - Flag that indicates whether to overwrite the destination file. + * - -h, --help + - + - false + - help for download + * - --out + - string + - true + - Output file name. This value defaults to the log name. + * - --projectId + - string + - false + - Hexadecimal string that identifies the project to use. This option overrides the settings in the configuration file or environment variable. + * - --start + - int + - false + - UNIX Epoch-formatted starting date and time for the range of log messages to retrieve. This value defaults to 24 hours prior to the current timestamp. + +Inherited Options +----------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 10 10 60 + + * - Name + - Type + - Required + - Description + * - -P, --profile + - string + - false + - Name of the profile to use from your configuration file. To learn about profiles for the Atlas CLI, see https://dochub.mongodb.org/core/atlas-cli-save-connection-settings. + +Output +------ + +If the command succeeds, the CLI returns output similar to the following sample. Values in brackets represent your values. + +.. code-block:: + + Download of completed. + + +Examples +-------- + +.. code-block:: + + # Download the audit log file from the instance myProcessor for the project with the ID 5e2211c17a3e5a48f5497de3: + atlas streams instance download myProcessor --projectId 5e2211c17a3e5a48f5497de3 diff --git a/docs/command/atlas-streams-instances.txt b/docs/command/atlas-streams-instances.txt index 03adcf2697..88cb43a8a5 100644 --- a/docs/command/atlas-streams-instances.txt +++ b/docs/command/atlas-streams-instances.txt @@ -54,6 +54,7 @@ Related Commands * :ref:`atlas-streams-instances-create` - Create an Atlas Stream Processing instance for your project * :ref:`atlas-streams-instances-delete` - Delete an Atlas Stream Processing instance. * :ref:`atlas-streams-instances-describe` - Describe an Atlas Stream Processing instance for your project. +* :ref:`atlas-streams-instances-download` - Download a compressed file that contains the logs for the specified Atlas Stream Processing instance. * :ref:`atlas-streams-instances-list` - List all the Atlas Stream Processing instances for your project. * :ref:`atlas-streams-instances-update` - Updates an Atlas Stream Processing instance for your project. @@ -64,6 +65,7 @@ Related Commands create delete describe + download list update diff --git a/internal/cli/atlas/streams/instance/download.go b/internal/cli/atlas/streams/instance/download.go new file mode 100644 index 0000000000..48306d09e8 --- /dev/null +++ b/internal/cli/atlas/streams/instance/download.go @@ -0,0 +1,122 @@ +// Copyright 2024 MongoDB Inc +// +// 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 instance + +import ( + "context" + "fmt" + "io" + + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli/require" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/config" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/flag" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/usage" + "github.com/spf13/afero" + "github.com/spf13/cobra" + atlasv2 "go.mongodb.org/atlas-sdk/v20231115008/admin" +) + +var downloadMessage = "Download of %s completed.\n" + +type DownloadOpts struct { + cli.GlobalOpts + cli.DownloaderOpts + tenantName string + start int64 + end int64 + store store.StreamsDownloader +} + +func (opts *DownloadOpts) initStore(ctx context.Context) func() error { + return func() error { + var err error + opts.store, err = store.New(store.AuthenticatedPreset(config.Default()), store.WithContext(ctx)) + return err + } +} + +func (opts *DownloadOpts) Run() error { + params := atlasv2.DownloadStreamTenantAuditLogsApiParams{ + GroupId: opts.ConfigProjectID(), + TenantName: opts.tenantName, + } + + if opts.start != 0 { + params.StartDate = &opts.start + } + + if opts.end != 0 { + params.EndDate = &opts.end + } + + f, err := opts.store.DownloadAuditLog(¶ms) + if err != nil { + return err + } + + defer f.Close() + + out, err := opts.NewWriteCloser() + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, f) + return err +} + +// DownloadBuilder +// atlas streams download [tenantName] --projectId [projectID]. +func DownloadBuilder() *cobra.Command { + const argsN = 1 + opts := &DownloadOpts{} + opts.Fs = afero.NewOsFs() + cmd := &cobra.Command{ + Use: "download ", + Short: "Download a compressed file that contains the logs for the specified Atlas Stream Processing instance.", + Long: `This command downloads a file with a .gz extension. ` + fmt.Sprintf(usage.RequiredRole, "Project Data Access Read/Write"), + Args: cobra.MatchAll( + require.ExactArgs(argsN), + ), + Example: ` # Download the audit log file from the instance myProcessor for the project with the ID 5e2211c17a3e5a48f5497de3: + atlas streams instance download myProcessor --projectId 5e2211c17a3e5a48f5497de3`, + Annotations: map[string]string{ + "tenantNameDesc": "Label that identifies the tenant that stores the log files that you want to download.", + "output": downloadMessage, + }, + PreRunE: func(cmd *cobra.Command, args []string) error { + opts.tenantName = args[0] + return opts.PreRunE(opts.ValidateProjectID, opts.initStore(cmd.Context())) + }, + RunE: func(_ *cobra.Command, _ []string) error { + return opts.Run() + }, + } + + cmd.Flags().StringVar(&opts.Out, flag.Out, "", usage.LogOut) + cmd.Flags().Int64Var(&opts.start, flag.Start, 0, usage.LogStart) + cmd.Flags().Int64Var(&opts.end, flag.End, 0, usage.LogEnd) + cmd.Flags().BoolVar(&opts.Force, flag.Force, false, usage.ForceFile) + + cmd.Flags().StringVar(&opts.ProjectID, flag.ProjectID, "", usage.ProjectID) + + _ = cmd.MarkFlagRequired(flag.Out) + _ = cmd.MarkFlagFilename(flag.Out) + + return cmd +} diff --git a/internal/cli/atlas/streams/instance/download_test.go b/internal/cli/atlas/streams/instance/download_test.go new file mode 100644 index 0000000000..4a5370e58e --- /dev/null +++ b/internal/cli/atlas/streams/instance/download_test.go @@ -0,0 +1,84 @@ +// Copyright 2024 MongoDB Inc +// +// 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 instance + +import ( + "io" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/flag" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/mocks" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/test" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + atlasv2 "go.mongodb.org/atlas-sdk/v20231115008/admin" +) + +func TestDownloadOpts_Run(t *testing.T) { + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockStreamsDownloader(ctrl) + + const contents = "expected" + const projectID = "download-project-id" + const tenantName = "streams-tenant" + + fs := afero.NewMemMapFs() + + downloadOpts := &DownloadOpts{ + store: mockStore, + DownloaderOpts: cli.DownloaderOpts{ + Out: "auditLogs.gz", + Fs: fs, + }, + } + + downloadOpts.ProjectID = projectID + downloadOpts.tenantName = tenantName + + downloadParams := new(atlasv2.DownloadStreamTenantAuditLogsApiParams) + downloadParams.EndDate = nil + downloadParams.StartDate = nil + downloadParams.GroupId = projectID + downloadParams.TenantName = tenantName + + expected := io.NopCloser(strings.NewReader(contents)) + + mockStore. + EXPECT(). + DownloadAuditLog(downloadParams). + Return(expected, nil). + Times(1) + + if err := downloadOpts.Run(); err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } + + of, _ := fs.Open("auditLogs.gz") + defer of.Close() + b, _ := io.ReadAll(of) + require.Equal(t, contents, string(b)) +} + +func TestDownloadBuilder(t *testing.T) { + test.CmdValidator( + t, + DownloadBuilder(), + 0, + []string{flag.Out, flag.Start, flag.End, flag.Force, flag.ProjectID}, + ) +} diff --git a/internal/cli/atlas/streams/instance/instance.go b/internal/cli/atlas/streams/instance/instance.go index c21e2d4603..fa9b2be1ac 100644 --- a/internal/cli/atlas/streams/instance/instance.go +++ b/internal/cli/atlas/streams/instance/instance.go @@ -27,7 +27,7 @@ func Builder() *cobra.Command { Short: "Manage Atlas Stream Processing instances.", Long: `Create, list, update and delete your Atlas Stream Processing instances.`, } - cmd.AddCommand(CreateBuilder(), UpdateBuilder(), ListBuilder(), DeleteBuilder(), DescribeBuilder()) + cmd.AddCommand(CreateBuilder(), UpdateBuilder(), ListBuilder(), DeleteBuilder(), DescribeBuilder(), DownloadBuilder()) return cmd } diff --git a/internal/cli/atlas/streams/instance/instance_test.go b/internal/cli/atlas/streams/instance/instance_test.go index 685e4e4323..22bd648cab 100644 --- a/internal/cli/atlas/streams/instance/instance_test.go +++ b/internal/cli/atlas/streams/instance/instance_test.go @@ -24,7 +24,7 @@ func TestBuilder(t *testing.T) { test.CmdValidator( t, Builder(), - 5, + 6, []string{}, ) } diff --git a/internal/mocks/mock_streams.go b/internal/mocks/mock_streams.go index 34ce43220a..eafc79c8a1 100644 --- a/internal/mocks/mock_streams.go +++ b/internal/mocks/mock_streams.go @@ -1,10 +1,11 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store (interfaces: StreamsLister,StreamsDescriber,StreamsCreator,StreamsDeleter,StreamsUpdater,ConnectionCreator,ConnectionDeleter,ConnectionUpdater,StreamsConnectionDescriber,StreamsConnectionLister) +// Source: github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store (interfaces: StreamsLister,StreamsDescriber,StreamsCreator,StreamsDeleter,StreamsUpdater,StreamsDownloader,ConnectionCreator,ConnectionDeleter,ConnectionUpdater,StreamsConnectionDescriber,StreamsConnectionLister) // Package mocks is a generated GoMock package. package mocks import ( + io "io" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -200,6 +201,44 @@ func (mr *MockStreamsUpdaterMockRecorder) UpdateStream(arg0, arg1, arg2 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStream", reflect.TypeOf((*MockStreamsUpdater)(nil).UpdateStream), arg0, arg1, arg2) } +// MockStreamsDownloader is a mock of StreamsDownloader interface. +type MockStreamsDownloader struct { + ctrl *gomock.Controller + recorder *MockStreamsDownloaderMockRecorder +} + +// MockStreamsDownloaderMockRecorder is the mock recorder for MockStreamsDownloader. +type MockStreamsDownloaderMockRecorder struct { + mock *MockStreamsDownloader +} + +// NewMockStreamsDownloader creates a new mock instance. +func NewMockStreamsDownloader(ctrl *gomock.Controller) *MockStreamsDownloader { + mock := &MockStreamsDownloader{ctrl: ctrl} + mock.recorder = &MockStreamsDownloaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStreamsDownloader) EXPECT() *MockStreamsDownloaderMockRecorder { + return m.recorder +} + +// DownloadAuditLog mocks base method. +func (m *MockStreamsDownloader) DownloadAuditLog(arg0 *admin.DownloadStreamTenantAuditLogsApiParams) (io.ReadCloser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DownloadAuditLog", arg0) + ret0, _ := ret[0].(io.ReadCloser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadAuditLog indicates an expected call of DownloadAuditLog. +func (mr *MockStreamsDownloaderMockRecorder) DownloadAuditLog(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAuditLog", reflect.TypeOf((*MockStreamsDownloader)(nil).DownloadAuditLog), arg0) +} + // MockConnectionCreator is a mock of ConnectionCreator interface. type MockConnectionCreator struct { ctrl *gomock.Controller diff --git a/internal/store/streams.go b/internal/store/streams.go index a4d2e4b83f..0dec279749 100644 --- a/internal/store/streams.go +++ b/internal/store/streams.go @@ -15,10 +15,13 @@ package store import ( + "fmt" + "io" + atlasv2 "go.mongodb.org/atlas-sdk/v20231115008/admin" ) -//go:generate mockgen -destination=../mocks/mock_streams.go -package=mocks github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store StreamsLister,StreamsDescriber,StreamsCreator,StreamsDeleter,StreamsUpdater,ConnectionCreator,ConnectionDeleter,ConnectionUpdater,StreamsConnectionDescriber,StreamsConnectionLister +//go:generate mockgen -destination=../mocks/mock_streams.go -package=mocks github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store StreamsLister,StreamsDescriber,StreamsCreator,StreamsDeleter,StreamsUpdater,StreamsDownloader,ConnectionCreator,ConnectionDeleter,ConnectionUpdater,StreamsConnectionDescriber,StreamsConnectionLister type StreamsLister interface { ProjectStreams(*atlasv2.ListStreamInstancesApiParams) (*atlasv2.PaginatedApiStreamsTenant, error) @@ -40,6 +43,10 @@ type StreamsUpdater interface { UpdateStream(string, string, *atlasv2.StreamsDataProcessRegion) (*atlasv2.StreamsTenant, error) } +type StreamsDownloader interface { + DownloadAuditLog(*atlasv2.DownloadStreamTenantAuditLogsApiParams) (io.ReadCloser, error) +} + type StreamsConnectionLister interface { StreamsConnections(string, string) (*atlasv2.PaginatedApiStreamsConnection, error) } @@ -85,6 +92,17 @@ func (s *Store) UpdateStream(projectID, name string, streamsDataProcessRegion *a return result, err } +func (s *Store) DownloadAuditLog(request *atlasv2.DownloadStreamTenantAuditLogsApiParams) (io.ReadCloser, error) { + result, _, err := s.clientv2.StreamsApi.DownloadStreamTenantAuditLogsWithParams(s.ctx, request).Execute() + if err != nil { + return nil, err + } + if result == nil { + return nil, fmt.Errorf("returned file is empty") + } + return result, nil +} + // StreamsConnections encapsulates the logic to manage different cloud providers. func (s *Store) StreamsConnections(projectID, tenantName string) (*atlasv2.PaginatedApiStreamsConnection, error) { connections, _, err := s.clientv2.StreamsApi.ListStreamConnections(s.ctx, projectID, tenantName).Execute() diff --git a/test/README.md b/test/README.md index d83e17a9d2..6f01f4b203 100644 --- a/test/README.md +++ b/test/README.md @@ -193,6 +193,7 @@ | `streams instance describe` | Y | Y | `streams instance list` | Y | Y | `streams instance update` | Y | Y +| `streams instance log` | Y | Y | `config` | | | `completion` | Y | Y | `config delete` | Y | Y diff --git a/test/e2e/atlas/streams_test.go b/test/e2e/atlas/streams_test.go index d67f441351..d0b9d77d90 100644 --- a/test/e2e/atlas/streams_test.go +++ b/test/e2e/atlas/streams_test.go @@ -20,7 +20,9 @@ import ( "fmt" "os" "os/exec" + "strconv" "testing" + "time" "github.com/mongodb/mongodb-atlas-cli/atlascli/test/e2e" "github.com/stretchr/testify/assert" @@ -92,6 +94,28 @@ func TestStreams(t *testing.T) { assert.Equal(t, instance.GetName(), instanceName) }) + t.Run("Downloading streams instance logs instance", func(t *testing.T) { + cmd := exec.Command(cliPath, + "streams", + "instance", + "download", + instanceName, + "--out", + "-", + "--start", + strconv.FormatInt(time.Now().Add(-10*time.Second).Unix(), 10), + "--end", + strconv.FormatInt(time.Now().Unix(), 10), + "--force", + "--projectId", + g.projectID, + ) + cmd.Env = os.Environ() + + resp, err := cmd.CombinedOutput() + require.NoError(t, err, string(resp)) + }) + t.Run("List all streams in the e2e project after creating", func(t *testing.T) { cmd := exec.Command(cliPath, "streams",