diff --git a/pkg/minikube/audit/audit.go b/pkg/minikube/audit/audit.go index 83b0ee98c2c7..d1c5d07d73cb 100644 --- a/pkg/minikube/audit/audit.go +++ b/pkg/minikube/audit/audit.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/viper" "k8s.io/klog" "k8s.io/minikube/pkg/minikube/config" + "k8s.io/minikube/pkg/version" ) // userName pulls the user flag, if empty gets the os username. @@ -54,8 +55,8 @@ func Log(startTime time.Time) { if !shouldLog() { return } - e := newEntry(os.Args[1], args(), userName(), startTime, time.Now()) - if err := appendToLog(e); err != nil { + r := newRow(os.Args[1], args(), userName(), version.GetVersion(), startTime, time.Now()) + if err := appendToLog(r); err != nil { klog.Error(err) } } diff --git a/pkg/minikube/audit/entry.go b/pkg/minikube/audit/entry.go deleted file mode 100644 index 75b32af9a099..000000000000 --- a/pkg/minikube/audit/entry.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors 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 audit - -import ( - "time" - - "github.com/spf13/viper" - "k8s.io/minikube/pkg/minikube/config" - "k8s.io/minikube/pkg/minikube/constants" -) - -// entry represents the execution of a command. -type entry struct { - data map[string]string -} - -// Type returns the cloud events compatible type of this struct. -func (e *entry) Type() string { - return "io.k8s.sigs.minikube.audit" -} - -// newEntry returns a new audit type. -func newEntry(command string, args string, user string, startTime time.Time, endTime time.Time) *entry { - return &entry{ - map[string]string{ - "args": args, - "command": command, - "endTime": endTime.Format(constants.TimeFormat), - "profile": viper.GetString(config.ProfileName), - "startTime": startTime.Format(constants.TimeFormat), - "user": user, - }, - } -} diff --git a/pkg/minikube/audit/logFile.go b/pkg/minikube/audit/logFile.go index 1d2428441230..1762070f1e1f 100644 --- a/pkg/minikube/audit/logFile.go +++ b/pkg/minikube/audit/logFile.go @@ -30,7 +30,7 @@ var currentLogFile *os.File // setLogFile sets the logPath and creates the log file if it doesn't exist. func setLogFile() error { lp := localpath.AuditLog() - f, err := os.OpenFile(lp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + f, err := os.OpenFile(lp, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) if err != nil { return fmt.Errorf("unable to open %s: %v", lp, err) } @@ -38,15 +38,15 @@ func setLogFile() error { return nil } -// appendToLog appends the audit entry to the log file. -func appendToLog(entry *entry) error { +// appendToLog appends the row to the log file. +func appendToLog(row *row) error { if currentLogFile == nil { if err := setLogFile(); err != nil { return err } } - e := register.CloudEvent(entry, entry.data) - bs, err := e.MarshalJSON() + ce := register.CloudEvent(row, row.toMap()) + bs, err := ce.MarshalJSON() if err != nil { return fmt.Errorf("error marshalling event: %v", err) } diff --git a/pkg/minikube/audit/logFile_test.go b/pkg/minikube/audit/logFile_test.go index ccea362767ca..1d83a672813d 100644 --- a/pkg/minikube/audit/logFile_test.go +++ b/pkg/minikube/audit/logFile_test.go @@ -42,8 +42,8 @@ func TestLogFile(t *testing.T) { defer func() { currentLogFile = &oldLogFile }() currentLogFile = f - e := newEntry("start", "-v", "user1", time.Now(), time.Now()) - if err := appendToLog(e); err != nil { + r := newRow("start", "-v", "user1", "v0.17.1", time.Now(), time.Now()) + if err := appendToLog(r); err != nil { t.Fatalf("Error appendingToLog: %v", err) } diff --git a/pkg/minikube/audit/report.go b/pkg/minikube/audit/report.go new file mode 100644 index 000000000000..68167571261b --- /dev/null +++ b/pkg/minikube/audit/report.go @@ -0,0 +1,66 @@ +/* +Copyright 2020 The Kubernetes Authors 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 audit + +import ( + "bufio" + "fmt" +) + +// RawReport contains the information required to generate formatted reports. +type RawReport struct { + headers []string + rows []row +} + +// Report is created using the last n lines from the log file. +func Report(lastNLines int) (*RawReport, error) { + if lastNLines <= 0 { + return nil, fmt.Errorf("last n lines must be 1 or greater") + } + if currentLogFile == nil { + if err := setLogFile(); err != nil { + return nil, fmt.Errorf("failed to set the log file: %v", err) + } + } + var logs []string + s := bufio.NewScanner(currentLogFile) + for s.Scan() { + // pop off the earliest line if already at desired log length + if len(logs) == lastNLines { + logs = logs[1:] + } + logs = append(logs, s.Text()) + } + if err := s.Err(); err != nil { + return nil, fmt.Errorf("failed to read from audit file: %v", err) + } + rows, err := logsToRows(logs) + if err != nil { + return nil, fmt.Errorf("failed to convert logs to rows: %v", err) + } + r := &RawReport{ + []string{"Command", "Args", "Profile", "User", "Version", "Start Time", "End Time"}, + rows, + } + return r, nil +} + +// ASCIITable creates a formatted table using the headers and rows from the report. +func (rr *RawReport) ASCIITable() string { + return rowsToASCIITable(rr.rows, rr.headers) +} diff --git a/pkg/minikube/audit/report_test.go b/pkg/minikube/audit/report_test.go new file mode 100644 index 000000000000..cb5cbfe0af45 --- /dev/null +++ b/pkg/minikube/audit/report_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2020 The Kubernetes Authors 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 audit + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestReport(t *testing.T) { + f, err := ioutil.TempFile("", "audit.json") + if err != nil { + t.Fatalf("failed creating temporary file: %v", err) + } + defer os.Remove(f.Name()) + + s := `{"data":{"args":"-p mini1","command":"start","endTime":"Wed, 03 Feb 2021 15:33:05 MST","profile":"mini1","startTime":"Wed, 03 Feb 2021 15:30:33 MST","user":"user1"},"datacontenttype":"application/json","id":"9b7593cb-fbec-49e5-a3ce-bdc2d0bfb208","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.si gs.minikube.audit"} +{"data":{"args":"-p mini1","command":"start","endTime":"Wed, 03 Feb 2021 15:33:05 MST","profile":"mini1","startTime":"Wed, 03 Feb 2021 15:30:33 MST","user":"user1"},"datacontenttype":"application/json","id":"9b7593cb-fbec-49e5-a3ce-bdc2d0bfb208","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.si gs.minikube.audit"} +{"data":{"args":"--user user2","command":"logs","endTime":"Tue, 02 Feb 2021 16:46:20 MST","profile":"minikube","startTime":"Tue, 02 Feb 2021 16:46:00 MST","user":"user2"},"datacontenttype":"application/json","id":"fec03227-2484-48b6-880a-88fd010b5efd","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.sigs.minikube.audit"}` + + if _, err := f.WriteString(s); err != nil { + t.Fatalf("failed writing to file: %v", err) + } + if _, err := f.Seek(0, 0); err != nil { + t.Fatalf("failed seeking to start of file: %v", err) + } + + oldLogFile := *currentLogFile + defer func() { currentLogFile = &oldLogFile }() + currentLogFile = f + + wantedLines := 2 + r, err := Report(wantedLines) + if err != nil { + t.Fatalf("failed to create report: %v", err) + } + + if len(r.rows) != wantedLines { + t.Errorf("report has %d lines of logs, want %d", len(r.rows), wantedLines) + } +} diff --git a/pkg/minikube/audit/row.go b/pkg/minikube/audit/row.go new file mode 100644 index 000000000000..fb5991a0085a --- /dev/null +++ b/pkg/minikube/audit/row.go @@ -0,0 +1,126 @@ +/* +Copyright 2020 The Kubernetes Authors 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 audit + +import ( + "bytes" + "encoding/json" + "fmt" + "time" + + "github.com/olekukonko/tablewriter" + "github.com/spf13/viper" + "k8s.io/minikube/pkg/minikube/config" + "k8s.io/minikube/pkg/minikube/constants" +) + +// row is the log of a single command. +type row struct { + args string + command string + endTime string + profile string + startTime string + user string + version string + Data map[string]string `json:"data"` +} + +// Type returns the cloud events compatible type of this struct. +func (e *row) Type() string { + return "io.k8s.sigs.minikube.audit" +} + +// assignFields converts the map values to their proper fields, +// to be used when converting from JSON Cloud Event format. +func (e *row) assignFields() { + e.args = e.Data["args"] + e.command = e.Data["command"] + e.endTime = e.Data["endTime"] + e.profile = e.Data["profile"] + e.startTime = e.Data["startTime"] + e.user = e.Data["user"] + e.version = e.Data["version"] +} + +// toMap combines fields into a string map, +// to be used when converting to JSON Cloud Event format. +func (e *row) toMap() map[string]string { + return map[string]string{ + "args": e.args, + "command": e.command, + "endTime": e.endTime, + "profile": e.profile, + "startTime": e.startTime, + "user": e.user, + "version": e.version, + } +} + +// newRow creates a new audit row. +func newRow(command string, args string, user string, version string, startTime time.Time, endTime time.Time, profile ...string) *row { + p := viper.GetString(config.ProfileName) + if len(profile) > 0 { + p = profile[0] + } + return &row{ + args: args, + command: command, + endTime: endTime.Format(constants.TimeFormat), + profile: p, + startTime: startTime.Format(constants.TimeFormat), + user: user, + version: version, + } +} + +// toFields converts a row to an array of fields, +// to be used when converting to a table. +func (e *row) toFields() []string { + return []string{e.command, e.args, e.profile, e.user, e.version, e.startTime, e.endTime} +} + +// logsToRows converts audit logs into arrays of rows. +func logsToRows(logs []string) ([]row, error) { + rows := []row{} + for _, l := range logs { + r := row{} + if err := json.Unmarshal([]byte(l), &r); err != nil { + return nil, fmt.Errorf("failed to unmarshal %q: %v", l, err) + } + r.assignFields() + rows = append(rows, r) + } + return rows, nil +} + +// rowsToASCIITable converts rows into a formatted ASCII table. +func rowsToASCIITable(rows []row, headers []string) string { + c := [][]string{} + for _, r := range rows { + c = append(c, r.toFields()) + } + b := new(bytes.Buffer) + t := tablewriter.NewWriter(b) + t.SetHeader(headers) + t.SetAutoFormatHeaders(false) + t.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true}) + t.SetCenterSeparator("|") + t.AppendBulk(c) + t.Render() + return b.String() +} diff --git a/pkg/minikube/audit/row_test.go b/pkg/minikube/audit/row_test.go new file mode 100644 index 000000000000..88b54174dfa9 --- /dev/null +++ b/pkg/minikube/audit/row_test.go @@ -0,0 +1,139 @@ +/* +Copyright 2020 The Kubernetes Authors 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 audit + +import ( + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + "k8s.io/minikube/pkg/minikube/constants" +) + +func TestRow(t *testing.T) { + c := "start" + a := "--alsologtostderr" + p := "profile1" + u := "user1" + v := "v0.17.1" + st := time.Now() + stFormatted := st.Format(constants.TimeFormat) + et := time.Now() + etFormatted := et.Format(constants.TimeFormat) + + r := newRow(c, a, u, v, st, et, p) + + t.Run("NewRow", func(t *testing.T) { + tests := []struct { + key string + got string + want string + }{ + {"command", r.command, c}, + {"args", r.args, a}, + {"profile", r.profile, p}, + {"user", r.user, u}, + {"version", r.version, v}, + {"startTime", r.startTime, stFormatted}, + {"endTime", r.endTime, etFormatted}, + } + + for _, tt := range tests { + if tt.got != tt.want { + t.Errorf("row.%s = %s; want %s", tt.key, tt.got, tt.want) + } + } + }) + + t.Run("Type", func(t *testing.T) { + got := r.Type() + want := "io.k8s.sigs.minikube.audit" + + if got != want { + t.Errorf("Type() = %s; want %s", got, want) + } + }) + + t.Run("toMap", func(t *testing.T) { + m := r.toMap() + + tests := []struct { + key string + want string + }{ + {"command", c}, + {"args", a}, + {"profile", p}, + {"user", u}, + {"version", v}, + {"startTime", stFormatted}, + {"endTime", etFormatted}, + } + + for _, tt := range tests { + got := m[tt.key] + if got != tt.want { + t.Errorf("map[%s] = %s; want %s", tt.key, got, tt.want) + } + } + }) + + t.Run("toFields", func(t *testing.T) { + got := r.toFields() + gotString := strings.Join(got, ",") + want := []string{c, a, p, u, v, stFormatted, etFormatted} + wantString := strings.Join(want, ",") + + if gotString != wantString { + t.Errorf("toFields() = %s; want %s", gotString, wantString) + } + }) + + t.Run("assignFields", func(t *testing.T) { + l := fmt.Sprintf(`{"data":{"args":"%s","command":"%s","endTime":"%s","profile":"%s","startTime":"%s","user":"%s","version":"v0.17.1"},"datacontenttype":"application/json","id":"bc6ec9d4-0d08-4b57-ac3b-db8d67774768","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.sigs.minikube.audit"}`, a, c, etFormatted, p, stFormatted, u) + + r := &row{} + if err := json.Unmarshal([]byte(l), r); err != nil { + t.Fatalf("failed to unmarshal log: %v", err) + } + + r.assignFields() + + tests := []struct { + key string + got string + want string + }{ + {"command", r.command, c}, + {"args", r.args, a}, + {"profile", r.profile, p}, + {"user", r.user, u}, + {"version", r.version, v}, + {"startTime", r.startTime, stFormatted}, + {"endTime", r.endTime, etFormatted}, + } + + for _, tt := range tests { + if tt.got != tt.want { + t.Errorf("singleEntry.%s = %s; want %s", tt.key, tt.got, tt.want) + + } + } + }) +} diff --git a/pkg/minikube/logs/logs.go b/pkg/minikube/logs/logs.go index 43b4c08e6722..8f2085624925 100644 --- a/pkg/minikube/logs/logs.go +++ b/pkg/minikube/logs/logs.go @@ -29,6 +29,7 @@ import ( "github.com/pkg/errors" "k8s.io/klog/v2" + "k8s.io/minikube/pkg/minikube/audit" "k8s.io/minikube/pkg/minikube/bootstrapper" "k8s.io/minikube/pkg/minikube/command" "k8s.io/minikube/pkg/minikube/config" @@ -188,12 +189,29 @@ func Output(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.Cluster } } + if err := outputAudit(lines); err != nil { + klog.Error(err) + failed = append(failed, "audit") + } + if len(failed) > 0 { return fmt.Errorf("unable to fetch logs for: %s", strings.Join(failed, ", ")) } return nil } +// outputAudit displays the audit logs. +func outputAudit(lines int) error { + out.Step(style.Empty, "") + out.Step(style.Empty, "==> Audit <==") + r, err := audit.Report(lines) + if err != nil { + return fmt.Errorf("failed to create audit report: %v", err) + } + out.Step(style.Empty, r.ASCIITable()) + return nil +} + // logCommands returns a list of commands that would be run to receive the anticipated logs func logCommands(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, length int, follow bool) map[string]string { cmds := bs.LogCommands(cfg, bootstrapper.LogOptions{Lines: length, Follow: follow}) diff --git a/test/integration/functional_test.go b/test/integration/functional_test.go index d52ae2f864c7..e279ae4a4323 100644 --- a/test/integration/functional_test.go +++ b/test/integration/functional_test.go @@ -722,7 +722,7 @@ func validateLogsCmd(ctx context.Context, t *testing.T, profile string) { if err != nil { t.Errorf("%s failed: %v", rr.Command(), err) } - expectedWords := []string{"apiserver", "Linux", "kubelet"} + expectedWords := []string{"apiserver", "Linux", "kubelet", "Audit"} switch ContainerRuntime() { case "docker": expectedWords = append(expectedWords, "Docker") diff --git a/test/integration/version_upgrade_test.go b/test/integration/version_upgrade_test.go index 564803e87d61..01f02e122cdc 100644 --- a/test/integration/version_upgrade_test.go +++ b/test/integration/version_upgrade_test.go @@ -196,6 +196,14 @@ func TestStoppedBinaryUpgrade(t *testing.T) { if err != nil { t.Fatalf("upgrade from %s to HEAD failed: %s: %v", legacyVersion, rr.Command(), err) } + + t.Run("MinikubeLogs", func(t *testing.T) { + args := []string{"logs", "-p", profile} + rr, err = Run(t, exec.CommandContext(ctx, Target(), args...)) + if err != nil { + t.Fatalf("`minikube logs` after upgrade to HEAD from %s failed: %v", legacyVersion, err) + } + }) } // TestKubernetesUpgrade upgrades Kubernetes from oldest to newest