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
3 changes: 2 additions & 1 deletion pkg/minikube/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -54,7 +55,7 @@ func Log(startTime time.Time) {
if !shouldLog() {
return
}
e := newEntry(os.Args[1], args(), userName(), startTime, time.Now())
e := newEntry(os.Args[1], args(), userName(), version.GetVersion(), startTime, time.Now())
if err := appendToLog(e); err != nil {
klog.Error(err)
}
Expand Down
104 changes: 90 additions & 14 deletions pkg/minikube/audit/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,109 @@ limitations under the License.
package audit

import (
"bytes"
"encoding/json"
"fmt"
"time"

"github.com/olekukonko/tablewriter"
"github.com/spf13/viper"
"k8s.io/klog"
"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
// singleEntry is the log entry of a single command.
type singleEntry struct {
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
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 *entry) Type() string {
func (e *singleEntry) Type() string {
return "io.k8s.sigs.minikube.audit"
}

// assignFields converts the map values to their proper fields
func (e *singleEntry) 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
func (e *singleEntry) 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,
}
}

// 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,
},
func newEntry(command string, args string, user string, version string, startTime time.Time, endTime time.Time, profile ...string) *singleEntry {
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
p := viper.GetString(config.ProfileName)
if len(profile) > 0 {
p = profile[0]
}
return &singleEntry{
args: args,
command: command,
endTime: endTime.Format(constants.TimeFormat),
profile: p,
startTime: startTime.Format(constants.TimeFormat),
user: user,
version: version,
}
}

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

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

// entriesToTable converts audit lines into a formatted table.
func entriesToTable(entries []singleEntry, headers []string) string {
c := [][]string{}
for _, e := range entries {
c = append(c, e.toFields())
}
klog.Info(c)
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()
}
139 changes: 139 additions & 0 deletions pkg/minikube/audit/entry_test.go
Original file line number Diff line number Diff line change
@@ -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 TestEntry(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)

e := newEntry(c, a, u, v, st, et, p)

t.Run("TestNewEntry", func(t *testing.T) {
tests := []struct {
key string
got string
want string
}{
{"command", e.command, c},
{"args", e.args, a},
{"profile", e.profile, p},
{"user", e.user, u},
{"version", e.version, v},
{"startTime", e.startTime, stFormatted},
{"endTime", e.endTime, etFormatted},
}

for _, tt := range tests {
if tt.got != tt.want {
t.Errorf("singleEntry.%s = %s; want %s", tt.key, tt.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("TestToMap", func(t *testing.T) {
m := e.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("TestToField", func(t *testing.T) {
got := e.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("TestAssignFields", 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)

e := &singleEntry{}
if err := json.Unmarshal([]byte(l), e); err != nil {
t.Fatalf("failed to unmarshal log:: %v", err)
}

e.assignFields()

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

for _, tt := range tests {
if tt.got != tt.want {
t.Errorf("singleEntry.%s = %s; want %s", tt.key, tt.got, tt.want)

}
}
})
}
6 changes: 3 additions & 3 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 @@ -39,13 +39,13 @@ func setLogFile() error {
}

// appendToLog appends the audit entry to the log file.
func appendToLog(entry *entry) error {
func appendToLog(entry *singleEntry) error {
if currentLogFile == nil {
if err := setLogFile(); err != nil {
return err
}
}
e := register.CloudEvent(entry, entry.data)
e := register.CloudEvent(entry, entry.toMap())
bs, err := e.MarshalJSON()
if err != nil {
return fmt.Errorf("error marshalling event: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/minikube/audit/logFile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestLogFile(t *testing.T) {
defer func() { currentLogFile = &oldLogFile }()
currentLogFile = f

e := newEntry("start", "-v", "user1", time.Now(), time.Now())
e := newEntry("start", "-v", "user1", "v0.17.1", time.Now(), time.Now())
if err := appendToLog(e); err != nil {
t.Fatalf("Error appendingToLog: %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
entries []singleEntry
}

// 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 logs []string
s := bufio.NewScanner(currentLogFile)
for s.Scan() {
// pop off the earliest line if already at desired log length
if len(logs) == lines {
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)
}
e, err := logsToEntries(logs)
if err != nil {
return nil, fmt.Errorf("failed to convert logs to entries: %v", err)
}
r := &Data{
[]string{"Command", "Args", "Profile", "User", "Version", "Start Time", "End Time"},
e,
}
return r, nil
}

// Table creates a formatted table using entries from the report.
func (r *Data) Table() string {
spowelljr marked this conversation as resolved.
Show resolved Hide resolved
return entriesToTable(r.entries, r.headers)
}
Loading