Merge pull request #10350 from spowelljr/addAudit

Add audit logs to minikube logs output
pull/10527/head
Medya Ghazizadeh 2021-02-19 12:12:42 -08:00 committed by GitHub
commit 26351ff6c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 424 additions and 59 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/spf13/viper"
"k8s.io/klog/v2"
"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)
}
}

View File

@ -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,
},
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

126
pkg/minikube/audit/row.go Normal file
View File

@ -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()
}

View File

@ -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)
}
}
})
}

View File

@ -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})

View File

@ -797,7 +797,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")

View File

@ -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