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

Add audit logs to minikube logs output #10350

Merged
merged 10 commits into from
Feb 19, 2021
42 changes: 41 additions & 1 deletion pkg/minikube/audit/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@ 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"
)

// entry represents the execution of a command.
type entry struct {
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
data map[string]string
Data map[string]string `json:"data"`
}

// Type returns the cloud events compatible type of this struct.
Expand All @@ -47,3 +51,39 @@ func newEntry(command string, args string, user string, startTime time.Time, end
},
}
}

// toFields converts an entry to an array of fields.
func (e *entry) toFields() []string {
d := e.Data
return []string{d["command"], d["args"], d["profile"], d["user"], d["startTime"], d["endTime"]}
}

// logsToFields converts audit logs into arrays of fields.
func logsToFields(logs []string) ([][]string, error) {
c := [][]string{}
for _, l := range logs {
e := &entry{}
if err := json.Unmarshal([]byte(l), e); err != nil {
return nil, fmt.Errorf("failed to unmarshal %q: %v", l, err)
}
c = append(c, e.toFields())
}
return c, nil
}

// logsToTable converts audit lines into a formatted table.
func logsToTable(logs []string, headers []string) (string, error) {
f, err := logsToFields(logs)
if err != nil {
return "", fmt.Errorf("failed to convert logs to fields: %v", err)
}
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(f)
t.Render()
return b.String(), nil
}
88 changes: 88 additions & 0 deletions pkg/minikube/audit/entry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
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 (
"strings"
"testing"
"time"

"github.com/spf13/viper"
"k8s.io/minikube/pkg/minikube/config"
"k8s.io/minikube/pkg/minikube/constants"
)

func TestEntry(t *testing.T) {
c := "start"
a := "--alsologtostderr"
p := "profile1"
u := "user1"
st := time.Now()
stFormatted := st.Format(constants.TimeFormat)
et := time.Now()
etFormatted := et.Format(constants.TimeFormat)

// save current profile in case something depends on it
oldProfile := viper.GetString(config.ProfileName)
viper.Set(config.ProfileName, p)
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
e := newEntry(c, a, u, st, et)
viper.Set(config.ProfileName, oldProfile)

t.Run("TestNewEntry", func(t *testing.T) {
d := e.Data

tests := []struct {
key string
want string
}{
{"command", c},
{"args", a},
{"profile", p},
{"user", u},
{"startTime", stFormatted},
{"endTime", etFormatted},
}

for _, tt := range tests {
got := d[tt.key]

if got != tt.want {
t.Errorf("Data[%q] = %s; want %s", tt.key, got, tt.want)
}
}
})

t.Run("TestType", func(t *testing.T) {
got := e.Type()
want := "io.k8s.sigs.minikube.audit"

if got != want {
t.Errorf("Type() = %s; want %s", got, want)
}
})

t.Run("TestToField", func(t *testing.T) {
got := e.toFields()
gotString := strings.Join(got, ",")
want := []string{c, a, p, u, stFormatted, etFormatted}
wantString := strings.Join(want, ",")

if gotString != wantString {
t.Errorf("toFields() = %s; want %s", gotString, wantString)
}
})
}
4 changes: 2 additions & 2 deletions pkg/minikube/audit/logFile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("unable to open %s: %v", lp, err)
}
Expand All @@ -45,7 +45,7 @@ func appendToLog(entry *entry) error {
return err
}
}
e := register.CloudEvent(entry, entry.data)
e := register.CloudEvent(entry, entry.Data)
bs, err := e.MarshalJSON()
if err != nil {
return fmt.Errorf("error marshalling event: %v", err)
Expand Down
65 changes: 65 additions & 0 deletions pkg/minikube/audit/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
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"
)

type Data struct {
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
headers []string
logs []string
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
}

// Report is created from the log file.
func Report(lines int) (*Data, error) {
if lines <= 0 {
return nil, fmt.Errorf("number of 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 l []string
s := bufio.NewScanner(currentLogFile)
for s.Scan() {
// pop off the earliest line if already at desired log length
if len(l) == lines {
l = l[1:]
}
l = append(l, s.Text())
}
if err := s.Err(); err != nil {
return nil, fmt.Errorf("failed to read from audit file: %v", err)
}
r := &Data{
[]string{"Command", "Args", "Profile", "User", "Start Time", "End Time"},
l,
}
return r, nil
}

// Table creates a formatted table using last n logs from the report.
func (r *Data) Table() (string, error) {
t, err := logsToTable(r.logs, r.headers)
if err != nil {
return "", fmt.Errorf("failed to convert logs to table: %v", err)
}
return t, nil
}
62 changes: 62 additions & 0 deletions pkg/minikube/audit/report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Copyright 2020 The Kubernetes Authors All rights reserved.

spowelljr marked this conversation as resolved.
Show resolved Hide resolved
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 TestAuditReport(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.logs) != wantedLines {
t.Fatalf("report has %d lines of logs, want %d", len(r.logs), wantedLines)
}

t.Run("TestTable", func(t *testing.T) {
if _, err := r.Table(); err != nil {
t.Errorf("failed to create table from report: %v", err)
}
})
}
22 changes: 22 additions & 0 deletions pkg/minikube/logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -188,12 +189,33 @@ 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 with error: %v", err)
}
t, err := r.Table()
if err != nil {
return fmt.Errorf("failed to create audit table with error: %v", err)
}
out.Step(style.Empty, t)
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})
Expand Down