Check for new versions and upgrade from webgui. closes #1

This commit is contained in:
Arjan H
2020-04-12 17:42:51 +02:00
parent a2f50c3dc4
commit 5d23559849
7 changed files with 219 additions and 41 deletions

View File

@@ -199,6 +199,9 @@ case $txt in
"server-shutdown")
halt
;;
"version-update")
/home/labca/labca/install &>>$LOGFILE
;;
*)
echo "Unknown command '$txt'. ERROR!"
exit 1
@@ -206,8 +209,3 @@ case $txt in
esac
echo "ok"
# TODO:
# upgrade the labca stuff

View File

@@ -3,6 +3,7 @@ package main
import (
"bufio"
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
@@ -11,7 +12,9 @@ import (
"errors"
"fmt"
"github.com/biz/templates"
"github.com/dustin/go-humanize"
_ "github.com/go-sql-driver/mysql"
"github.com/google/go-github/github"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
@@ -39,20 +42,23 @@ import (
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
updateInterval = 24 * time.Hour
)
var (
appSession *sessions.Session
restartSecret string
sessionStore *sessions.CookieStore
tmpls *templates.Templates
version string
dbConn string
dbType string
isDev bool
appSession *sessions.Session
restartSecret string
sessionStore *sessions.CookieStore
tmpls *templates.Templates
version string
dbConn string
dbType string
isDev bool
updateAvailable bool
updateChecked time.Time
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
@@ -232,6 +238,43 @@ func errorHandler(w http.ResponseWriter, r *http.Request, err error, status int)
}
}
func checkUpdates(forced bool) ([]string, []string) {
var versions []string
var descriptions []string
if forced || updateChecked.Add(updateInterval).Before(time.Now()) {
latest := ""
newer := true
client := github.NewClient(nil)
if releases, _, err := client.Repositories.ListReleases(context.Background(), "hakwerk", "labca", nil); err == nil {
for i := 0; i < len(releases); i++ {
release := releases[i]
if !*release.Draft {
if !*release.Prerelease || isDev {
if latest == "" {
latest = *release.Name
}
if *release.Name == version {
newer = false
}
if newer {
versions = append(versions, *release.Name)
descriptions = append(descriptions, *release.Body)
}
}
}
}
updateChecked = time.Now()
updateAvailable = (len(releases) > 0) && (latest != version)
}
}
return versions, descriptions
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if !viper.GetBool("config.complete") {
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
@@ -240,6 +283,10 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
dashboardData, err := CollectDashboardData(w, r)
if err == nil {
checkUpdates(false)
dashboardData["UpdateAvailable"] = updateAvailable
dashboardData["UpdateChecked"] = humanize.RelTime(updateChecked, time.Now(), "", "")
render(w, r, "dashboard", dashboardData)
}
}
@@ -798,6 +845,24 @@ func (res *Result) ManageComponents(w http.ResponseWriter, r *http.Request, acti
}
}
func _checkUpdatesHandler(w http.ResponseWriter, r *http.Request) {
res := struct {
Success bool
UpdateAvailable bool
UpdateChecked string
Versions []string
Descriptions []string
Errors map[string]string
}{Success: true, Errors: make(map[string]string)}
res.Versions, res.Descriptions = checkUpdates(true)
res.UpdateAvailable = updateAvailable
res.UpdateChecked = humanize.RelTime(updateChecked, time.Now(), "", "")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string) bool {
if action == "backup-restore" || action == "backup-delete" || action == "backup-now" {
_backupHandler(w, r)
@@ -829,6 +894,11 @@ func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string)
return true
}
if action == "version-check" {
_checkUpdatesHandler(w, r)
return true
}
return false
}
@@ -858,6 +928,8 @@ func _managePost(w http.ResponseWriter, r *http.Request) {
"update-config",
"update-email",
"send-email",
"version-check",
"version-update",
} {
if a == action {
actionKnown = true
@@ -878,7 +950,7 @@ func _managePost(w http.ResponseWriter, r *http.Request) {
res.Message = "Command failed - see LabCA log for any details"
}
if action != "server-restart" && action != "server-shutdown" {
if action != "server-restart" && action != "server-shutdown" && action != "version-update" {
res.ManageComponents(w, r, action)
}
@@ -890,6 +962,10 @@ func _manageGet(w http.ResponseWriter, r *http.Request) {
manageData := make(map[string]interface{})
manageData["RequestBase"] = r.Header.Get("X-Request-Base")
checkUpdates(false)
manageData["UpdateAvailable"] = updateAvailable
manageData["UpdateChecked"] = humanize.RelTime(updateChecked, time.Now(), "", "")
components := _parseComponents(getLog(w, r, "components"))
for i := 0; i < len(components); i++ {
if components[i].Name == "NGINX Webserver" {
@@ -2292,6 +2368,8 @@ func init() {
dbType = viper.GetString("db.type")
version = viper.GetString("version")
updateAvailable = false
}
func main() {

View File

@@ -9,6 +9,7 @@ if [ ! -e bin/labca ]; then
go get github.com/biz/templates
go get github.com/go-sql-driver/mysql
go get github.com/dustin/go-humanize
go get github.com/google/go-github/github
go get github.com/gorilla/mux
go get github.com/gorilla/securecookie
go get github.com/gorilla/sessions

View File

@@ -127,7 +127,13 @@
<div class="col-lg-4">
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-fw fa-desktop"></i> System Overview
<span class="pull-left"><i class="fa fa-fw fa-desktop"></i> System Overview</span>
{{ if .UpdateAvailable }}
<span class="pull-right">
<a class="update" href="{{ .RequestBase }}/manage"><i class="fa fa-fw fa-rocket"></i> <i>update available!</i></a>
</span>
{{ end }}
<div class="clearfix"></div>
</div>
<div class="panel-body">
<div class="table-responsive">

View File

@@ -43,6 +43,16 @@
{{ range $btn := $item.Buttons }}
<button class="btn btn-outline btn-reg {{ $btn.Class }}" type="button" id="{{ $btn.Id }}" title="{{ $btn.Title }}">{{ $btn.Label }}</button>
{{ end }}
{{ if eq $item.Name "LabCA Application" }}
<br/>
<button class="btn btn-outline btn-wide btn-warning mt5 {{ if not $.UpdateAvailable }}hidden{{ end }}" type="button" id="version-update" title="Update to latest version">Update LabCA</button>
<br/>
<button class="btn btn-outline btn-wide btn-success mt5" type="button" id="version-check" title="Check if there is a newer version available">Check for updates</button>
<span id="update-checked-span">
<br/>
Last checked: <span id="update-last-checked">{{ $.UpdateChecked }}</span>
</span>
{{ end }}
</td>
</tr>
{{ end }}
@@ -262,7 +272,7 @@
<span class="hidden" id="request-base">{{ .RequestBase }}</span>
<div class="modal fade bd-modal-lg" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div id="modal-spinner" class="modal fade bd-modal-lg" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content" style="width: 48px">
<img id="manage-spinner" src="static/img/spinner.gif" height="36">
@@ -275,6 +285,7 @@
<script type="text/javascript" src="static/js/zxcvbn.js"></script>
<script type="text/javascript" src="static/js/pwdux.js"></script>
<script type="text/javascript" src="static/js/jquery.stickytabs.js"></script>
<script type="text/javascript" src="static/js/bootstrap-dialog.min.js"></script>
<script>
$(function() {
$('.nav-tabs-sticky').stickyTabs();
@@ -292,7 +303,13 @@
});
$(".btn").click(function(evt) {
$('.modal').modal('show');
$('#modal-spinner').modal('show');
if ($(evt.target).hasClass('btn-warning') || $(evt.target).hasClass('btn-danger')) {
if (!window.confirm("Are you sure?")) {
$('#modal-spinner').modal('hide');
return false;
}
}
$(evt.target).blur();
$("#backup-result").hide();
$("#export-pwd-err").hide();
@@ -314,7 +331,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully created backup";
@@ -327,7 +344,7 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#backup-result").removeClass("hidden").removeClass("success").show().text(err).addClass("error");
});
@@ -342,7 +359,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully restored backup, restarting server now...";
@@ -356,7 +373,7 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#backup-result").removeClass("hidden").removeClass("success").show().text(err).addClass("error");
});
@@ -372,7 +389,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully removed backup file";
@@ -384,7 +401,7 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#backup-result").removeClass("hidden").removeClass("success").show().text(err).addClass("error");
});
@@ -392,10 +409,10 @@
type = ($("#export-zip").prop('checked') ? "zip" : ($("#export-pfx").prop('checked') ? "pfx" : "none"));
if ($("#export-pwd").val().length < 4) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#export-pwd-err").removeClass("hidden").show().text("Password needs to be at least 4 characters!");
} else if ($("#export-pwd").val() != $("#export-pwd2").val()) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#export-pwd2-err").removeClass("hidden").show().text("Passwords are not the same!");
} else {
var req = new XMLHttpRequest();
@@ -404,7 +421,7 @@
req.responseType = "blob";
req.onload = function (event) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
var blob = req.response;
var fileName = null;
@@ -430,7 +447,7 @@
};
req.onerror = function (event) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#cert-export-err").removeClass("hidden").show().text("Oops, something went wrong...");
};
@@ -451,7 +468,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully updated account.";
@@ -479,7 +496,7 @@
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#update-account-result").removeClass("hidden").removeClass("success").show().text(err).addClass("error");
});
@@ -498,7 +515,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully updated configuration.<br/>";
@@ -518,14 +535,14 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#update-config-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "update-email") {
form = $(evt.target).parent().parent()[0]
if (!form.checkValidity()) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
const tmpSubmit = document.createElement('button')
form.appendChild(tmpSubmit)
tmpSubmit.click()
@@ -545,7 +562,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully updated configuration.<br/>";
@@ -563,7 +580,7 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#update-email-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
}
@@ -576,7 +593,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully sent test email.<br/>";
@@ -594,10 +611,62 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#send-email-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "version-check") {
$.ajax(window.location.href, {
method: "POST",
data: {
action: $(evt.target).attr("id"),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
if (data.Success) {
if (data.UpdateAvailable) {
$("#version-update").removeClass("hidden");
var notes = "<span class=\"rel-notes-title\">RELEASE NOTES</span><br/><br/>";
jQuery.each(data.Versions, function(idx, val) {
notes += "<span class=\"rel-notes-title\">" + val + "</span><br/>"
notes += "<span>" + data.Descriptions[idx] + "</span><br/><br/>"
});
BootstrapDialog.show({
title: 'New version available!',
message: notes,
buttons: [{
label: 'OK',
cssClass: 'btn-outline btn-success',
action: function(dialogRef){
dialogRef.close();
}
}]
});
} else {
$("#version-update").addClass("hidden")
BootstrapDialog.show({
title: 'No new version',
message: 'There is currently no newer version available.',
buttons: [{
label: 'OK',
cssClass: 'btn-outline btn-success',
action: function(dialogRef){
dialogRef.close();
}
}]
});
}
$("#update-last-checked").text(data.UpdateChecked)
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
});
} else {
$.ajax(window.location.href, {
method: "POST",
@@ -606,7 +675,7 @@
},
})
.done(function(data) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
td = $(evt.target).parent().parent().find("td:eq(1)");
$(td).attr("title", data.Timestamp);
@@ -630,7 +699,7 @@
}
})
.fail(function(xhr, status, err) {
$('.modal').modal('hide');
$('#modal-spinner').modal('hide');
$("#cert-export-err").removeClass("hidden").show().text(err);
});
}

View File

@@ -83,6 +83,10 @@ p.caption {
margin-bottom: 15px;
}
.mt5 {
margin-top: 5px;
}
.mt20 {
margin-top: 20px;
}
@@ -126,3 +130,24 @@ p.caption {
position: relative;
z-index: 2;
}
a.update {
color: darkgreen;
}
a.update:hover {
text-decoration: none;
}
.bootstrap-dialog .modal-header {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.bootstrap-dialog.type-primary .modal-header {
background-color: #f5f5f5;
}
.rel-notes-title {
font-weight: bold;
}

1
www/js/bootstrap-dialog.min.js vendored Normal file

File diff suppressed because one or more lines are too long