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
5 changes: 3 additions & 2 deletions 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,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)
}
}
Expand Down
49 changes: 0 additions & 49 deletions pkg/minikube/audit/entry.go

This file was deleted.

10 changes: 5 additions & 5 deletions pkg/minikube/audit/logFile.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,23 @@ 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)
}
currentLogFile = f
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)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/minikube/audit/logFile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
66 changes: 66 additions & 0 deletions pkg/minikube/audit/report.go
Original file line number Diff line number Diff line change
@@ -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)
}
56 changes: 56 additions & 0 deletions pkg/minikube/audit/report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
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 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 changes: 126 additions & 0 deletions pkg/minikube/audit/row.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading